From ded9d935ce5b22ce18dd0a9885da9056f80af5cf Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jakub=20Tr=C3=A1vn=C3=ADk?=
Date: Tue, 16 Dec 2025 13:07:00 +0100
Subject: [PATCH] Merge main into config-export-feature
---
.github/copilot-instructions.md | 1 +
.github/workflows/release.yml | 4 +-
AGENTS.md | 267 ++++++
Dockerfile | 6 +-
README.md | 10 +-
.../api-client/@tanstack/react-query.gen.ts | 57 +-
app/client/api-client/sdk.gen.ts | 36 +-
app/client/api-client/types.gen.ts | 340 +++++++-
.../components/create-repository-form.tsx | 784 -----------------
app/client/components/create-volume-form.tsx | 593 -------------
app/client/components/restore-form.tsx | 1 +
app/client/components/snapshots-table.tsx | 160 ++--
app/client/components/volume-icon.tsx | 6 +
app/client/hooks/use-server-events.ts | 33 +-
.../components/create-schedule-form.tsx | 138 ++-
.../components/schedule-mirrors-config.tsx | 352 ++++++++
.../schedule-notifications-config.tsx | 19 +-
.../backups/components/schedule-summary.tsx | 28 +-
.../components/snapshot-file-browser.tsx | 4 +-
.../modules/backups/routes/backup-details.tsx | 33 +-
app/client/modules/backups/routes/backups.tsx | 23 +-
.../modules/backups/routes/create-backup.tsx | 5 +-
.../components/create-notification-form.tsx | 4 +-
.../routes/create-notification.tsx | 3 +-
.../routes/notification-details.tsx | 14 +-
.../notifications/routes/notifications.tsx | 2 -
.../components/create-repository-form.tsx | 275 ++++++
.../azure-repository-form.tsx | 78 ++
.../repository-forms/gcs-repository-form.tsx | 65 ++
.../components/repository-forms/index.ts | 8 +
.../local-repository-form.tsx | 103 +++
.../repository-forms/r2-repository-form.tsx | 80 ++
.../rclone-repository-form.tsx | 96 ++
.../repository-forms/rest-repository-form.tsx | 78 ++
.../repository-forms/s3-repository-form.tsx | 78 ++
.../repository-forms/sftp-repository-form.tsx | 101 +++
.../repositories/routes/create-repository.tsx | 10 +-
.../repositories/routes/repositories.tsx | 4 +-
.../routes/repository-details.tsx | 20 +-
app/client/modules/repositories/tabs/info.tsx | 9 +-
.../modules/repositories/tabs/snapshots.tsx | 13 +-
.../modules/settings/routes/settings.tsx | 5 +-
.../volumes/components/create-volume-form.tsx | 218 +++++
.../volumes/components/healthchecks-card.tsx | 3 +-
.../volume-forms/directory-form.tsx | 51 ++
.../volumes/components/volume-forms/index.ts | 5 +
.../components/volume-forms/nfs-form.tsx | 120 +++
.../components/volume-forms/rclone-form.tsx | 127 +++
.../components/volume-forms/smb-form.tsx | 163 ++++
.../components/volume-forms/webdav-form.tsx | 146 ++++
.../modules/volumes/routes/create-volume.tsx | 5 +-
.../modules/volumes/routes/volume-details.tsx | 5 +-
app/client/modules/volumes/routes/volumes.tsx | 2 -
app/client/modules/volumes/tabs/info.tsx | 8 +-
app/drizzle/0018_breezy_invaders.sql | 139 +++
app/drizzle/0019_secret_nomad.sql | 24 +
app/drizzle/0020_even_dexter_bennett.sql | 1 +
app/drizzle/0021_steady_viper.sql | 1 +
app/drizzle/meta/0018_snapshot.json | 792 +++++++++++++++++
app/drizzle/meta/0019_snapshot.json | 807 +++++++++++++++++
app/drizzle/meta/0020_snapshot.json | 815 +++++++++++++++++
app/drizzle/meta/0021_snapshot.json | 823 ++++++++++++++++++
app/drizzle/meta/_journal.json | 290 +++---
app/schemas/notifications.ts | 4 +-
app/schemas/volumes.ts | 10 +-
app/server/core/capabilities.ts | 16 +-
app/server/core/config.ts | 4 +-
app/server/core/events.ts | 8 +
app/server/core/repository-mutex.ts | 180 ++++
app/server/db/db.ts | 4 +-
app/server/db/schema.ts | 42 +-
app/server/jobs/repository-healthchecks.ts | 6 +
app/server/modules/backends/backend.ts | 4 +
.../modules/backends/rclone/rclone-backend.ts | 128 +++
.../modules/backends/smb/smb-backend.ts | 5 +-
.../modules/backends/webdav/webdav-backend.ts | 4 +-
.../modules/backups/backups.controller.ts | 28 +-
app/server/modules/backups/backups.dto.ts | 91 ++
app/server/modules/backups/backups.service.ts | 280 +++++-
.../modules/events/events.controller.ts | 24 +
app/server/modules/lifecycle/startup.ts | 2 +-
.../modules/notifications/builders/email.ts | 7 +-
.../notifications/notifications.dto.ts | 2 +
.../notifications/notifications.service.ts | 8 +-
.../repositories/repositories.controller.ts | 2 +
.../modules/repositories/repositories.dto.ts | 1 +
.../repositories/repositories.service.ts | 188 ++--
app/server/modules/volumes/volume.service.ts | 30 +-
app/server/utils/backend-compatibility.ts | 148 ++++
app/server/utils/crypto.ts | 10 +-
app/server/utils/logger.ts | 6 +-
app/server/utils/restic.ts | 112 ++-
biome.json | 3 +-
bun.lock | 285 ++----
docker-compose.yml | 1 +
package.json | 72 +-
96 files changed, 8124 insertions(+), 2082 deletions(-)
create mode 100644 .github/copilot-instructions.md
create mode 100644 AGENTS.md
delete mode 100644 app/client/components/create-repository-form.tsx
delete mode 100644 app/client/components/create-volume-form.tsx
create mode 100644 app/client/modules/backups/components/schedule-mirrors-config.tsx
create mode 100644 app/client/modules/repositories/components/create-repository-form.tsx
create mode 100644 app/client/modules/repositories/components/repository-forms/azure-repository-form.tsx
create mode 100644 app/client/modules/repositories/components/repository-forms/gcs-repository-form.tsx
create mode 100644 app/client/modules/repositories/components/repository-forms/index.ts
create mode 100644 app/client/modules/repositories/components/repository-forms/local-repository-form.tsx
create mode 100644 app/client/modules/repositories/components/repository-forms/r2-repository-form.tsx
create mode 100644 app/client/modules/repositories/components/repository-forms/rclone-repository-form.tsx
create mode 100644 app/client/modules/repositories/components/repository-forms/rest-repository-form.tsx
create mode 100644 app/client/modules/repositories/components/repository-forms/s3-repository-form.tsx
create mode 100644 app/client/modules/repositories/components/repository-forms/sftp-repository-form.tsx
create mode 100644 app/client/modules/volumes/components/create-volume-form.tsx
create mode 100644 app/client/modules/volumes/components/volume-forms/directory-form.tsx
create mode 100644 app/client/modules/volumes/components/volume-forms/index.ts
create mode 100644 app/client/modules/volumes/components/volume-forms/nfs-form.tsx
create mode 100644 app/client/modules/volumes/components/volume-forms/rclone-form.tsx
create mode 100644 app/client/modules/volumes/components/volume-forms/smb-form.tsx
create mode 100644 app/client/modules/volumes/components/volume-forms/webdav-form.tsx
create mode 100644 app/drizzle/0018_breezy_invaders.sql
create mode 100644 app/drizzle/0019_secret_nomad.sql
create mode 100644 app/drizzle/0020_even_dexter_bennett.sql
create mode 100644 app/drizzle/0021_steady_viper.sql
create mode 100644 app/drizzle/meta/0018_snapshot.json
create mode 100644 app/drizzle/meta/0019_snapshot.json
create mode 100644 app/drizzle/meta/0020_snapshot.json
create mode 100644 app/drizzle/meta/0021_snapshot.json
create mode 100644 app/server/core/repository-mutex.ts
create mode 100644 app/server/modules/backends/rclone/rclone-backend.ts
create mode 100644 app/server/utils/backend-compatibility.ts
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
new file mode 100644
index 00000000..11d4b9cf
--- /dev/null
+++ b/.github/copilot-instructions.md
@@ -0,0 +1 @@
+- This project uses the AGENTS.md file to give detailed information about the repository structure and development commands. Make sure to read this file before starting development.
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 16e8d9a0..dadfa1b3 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -62,8 +62,6 @@ jobs:
type=semver,pattern={{major}}.{{minor}}.{{patch}},prefix=v,enable=${{ needs.determine-release-type.outputs.release_type == 'release' }}
flavor: |
latest=${{ needs.determine-release-type.outputs.release_type == 'release' }}
- cache-from: type=registry,ref=ghcr.io/nicotsx/zerobyte:buildcache
- cache-to: type=registry,ref=ghcr.io/nicotsx/zerobyte:buildcache,mode=max
- name: Build and push images
uses: docker/build-push-action@v6
@@ -76,6 +74,8 @@ jobs:
labels: ${{ steps.meta.outputs.labels }}
build-args: |
APP_VERSION=${{ needs.determine-release-type.outputs.tagname }}
+ cache-from: type=registry,ref=ghcr.io/nicotsx/zerobyte:buildcache
+ cache-to: type=registry,ref=ghcr.io/nicotsx/zerobyte:buildcache,mode=max
publish-release:
runs-on: ubuntu-latest
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 00000000..136692d0
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,267 @@
+# AGENTS.md
+
+## Important instructions
+
+- Never create migration files manually. Always use the provided command to generate migrations
+- If you realize an automated migration is incorrect, make sure to remove all the associated entries from the `_journal.json` and the newly created files located in `app/drizzle/` before re-generating the migration
+
+## Project Overview
+
+Zerobyte is a backup automation tool built on top of Restic that provides a web interface for scheduling, managing, and monitoring encrypted backups. It supports multiple volume backends (NFS, SMB, WebDAV, local directories) and repository backends (S3, Azure, GCS, local, and rclone-based storage).
+
+## Technology Stack
+
+- **Runtime**: Bun 1.3.1
+- **Server**: Hono (web framework) with Bun runtime
+- **Client**: React Router v7 (SSR) with React 19
+- **Database**: SQLite with Drizzle ORM
+- **Validation**: ArkType for runtime schema validation
+- **Styling**: Tailwind CSS v4 + Radix UI components
+- **Architecture**: Unified application structure (not a monorepo)
+- **Code Quality**: Biome (formatter & linter)
+- **Containerization**: Docker with multi-stage builds
+
+## Repository Structure
+
+This is a unified application with the following structure:
+
+- `app/server` - Bun-based API server with Hono
+- `app/client` - React Router SSR frontend components and modules
+- `app/schemas` - Shared ArkType schemas for validation
+- `app/drizzle` - Database migrations
+
+### Type Checking
+
+```bash
+# Run type checking and generate React Router types
+bun run tsc
+```
+
+### Building
+
+```bash
+# Build for production
+bun run build
+```
+
+### Database Migrations
+
+```bash
+# Generate new migration from schema changes
+bun gen:migrations
+
+# Generate a custom empty migration
+bunx drizzle-kit generate --custom --name=fix-timestamps-to-ms
+
+```
+
+### API Client Generation
+
+```bash
+# Generate TypeScript API client from OpenAPI spec
+# Note: Server is always running don't need to start it separately
+bun run gen:api-client
+```
+
+### Code Quality
+
+```bash
+# Format and lint (Biome)
+bunx biome check --write .
+
+# Format only
+bunx biome format --write .
+
+# Lint only
+bunx biome lint .
+```
+
+## Architecture
+
+### Server Architecture
+
+The server follows a modular service-oriented architecture:
+
+**Entry Point**: `app/server/index.ts`
+
+- Initializes servers using `react-router-hono-server`:
+ 1. Main API server on port 4096 (REST API + serves static frontend)
+ 2. Docker volume plugin server on Unix socket `/run/docker/plugins/zerobyte.sock` (optional, if Docker is available)
+
+**Modules** (`app/server/modules/`):
+Each module follows a controller � service � database pattern:
+
+- `auth/` - User authentication and session management
+- `volumes/` - Volume mounting/unmounting (NFS, SMB, WebDAV, directories)
+- `repositories/` - Restic repository management (S3, Azure, GCS, local, rclone)
+- `backups/` - Backup schedule management and execution
+- `notifications/` - Notification system with multiple providers (Discord, email, Gotify, Ntfy, Slack, Pushover)
+- `driver/` - Docker volume plugin implementation
+- `events/` - Server-Sent Events for real-time updates
+- `system/` - System information and capabilities
+- `lifecycle/` - Application startup/shutdown hooks
+
+**Backends** (`app/server/modules/backends/`):
+Each volume backend (NFS, SMB, WebDAV, directory) implements mounting logic using system tools (mount.nfs, mount.cifs, davfs2).
+
+**Jobs** (`app/server/jobs/`):
+Cron-based background jobs managed by the Scheduler:
+
+- `backup-execution.ts` - Runs scheduled backups (every minute)
+- `cleanup-dangling.ts` - Removes stale mounts (hourly)
+- `healthchecks.ts` - Checks volume health (every 5 minutes)
+- `repository-healthchecks.ts` - Validates repositories (every 10 minutes)
+- `cleanup-sessions.ts` - Expires old sessions (daily)
+
+**Core** (`app/server/core/`):
+
+- `scheduler.ts` - Job scheduling system using node-cron
+- `capabilities.ts` - Detects available system features (Docker support, etc.)
+- `constants.ts` - Application-wide constants
+
+**Utils** (`app/server/utils/`):
+
+- `restic.ts` - Restic CLI wrapper with type-safe output parsing
+- `spawn.ts` - Safe subprocess execution helpers
+- `logger.ts` - Winston-based logging
+- `crypto.ts` - Encryption utilities
+- `errors.ts` - Error handling middleware
+
+**Database** (`app/server/db/`):
+
+- Uses Drizzle ORM with SQLite
+- Schema in `schema.ts` defines: volumes, repositories, backup schedules, notifications, users, sessions
+- Migrations: `app/drizzle/`
+
+### Client Architecture
+
+**Framework**: React Router v7 with SSR
+**Entry Point**: `app/root.tsx`
+
+The client uses:
+
+- TanStack Query for server state management
+- Auto-generated API client from OpenAPI spec (in `app/client/api-client/`)
+- Radix UI primitives with custom Tailwind styling
+- Server-Sent Events hook (`use-server-events.ts`) for real-time updates
+
+Routes are organized in feature modules at `app/client/modules/*/routes/`.
+
+### Shared Schemas
+
+`app/schemas/` contains ArkType schemas used by both client and server:
+
+- Volume configurations (NFS, SMB, WebDAV, directory)
+- Repository configurations (S3, Azure, GCS, local, rclone)
+- Restic command output parsing types
+- Backend status types
+
+These schemas provide runtime validation and TypeScript types.
+
+## Restic Integration
+
+Zerobyte is a wrapper around Restic for backup operations. Key integration points:
+
+**Repository Management**:
+
+- Creates/initializes Restic repositories via `restic init`
+- Supports multiple backends: local, S3, Azure Blob Storage, Google Cloud Storage, or any rclone-supported backend
+- Stores single encryption password in `/var/lib/zerobyte/restic/password` (auto-generated on first run)
+
+**Backup Operations**:
+
+- Executes `restic backup` with user-defined schedules (cron expressions)
+- Supports include/exclude patterns for selective backups
+- Parses JSON output for progress tracking and statistics
+- Implements retention policies via `restic forget --prune`
+
+**Repository Utilities** (`utils/restic.ts`):
+
+- `buildRepoUrl()` - Constructs repository URLs for different backends
+- `buildEnv()` - Sets environment variables (credentials, cache dir)
+- `ensurePassfile()` - Manages encryption password file
+- Type-safe parsing of Restic JSON output using ArkType schemas
+
+**Rclone Integration** (`app/server/modules/repositories/`):
+
+- Allows using any rclone backend as a Restic repository
+- Dynamically generates rclone config and passes via environment variables
+- Supports backends like Dropbox, Google Drive, OneDrive, Backblaze B2, etc.
+
+## Docker Volume Plugin
+
+When Docker socket is available (`/var/run/docker.sock`), Zerobyte registers as a Docker volume plugin:
+
+**Plugin Location**: `/run/docker/plugins/zerobyte.sock`
+**Implementation**: `app/server/modules/driver/driver.controller.ts`
+
+This allows other containers to mount Zerobyte volumes using Docker.
+
+The plugin implements the Docker Volume Plugin API v1.
+
+## Environment & Configuration
+
+**Runtime Environment Variables**:
+
+- Database path: `./data/zerobyte.db` (configurable via `drizzle.config.ts`)
+- Restic cache: `/var/lib/zerobyte/restic/cache`
+- Restic password: `/var/lib/zerobyte/restic/password`
+- Volume mounts: `/var/lib/zerobyte/mounts/`
+- Local repositories: `/var/lib/zerobyte/repositories/`
+
+**Capabilities Detection**:
+On startup, the server detects available capabilities (see `core/capabilities.ts`):
+
+- **Docker**: Requires `/var/run/docker.sock` access
+- System will gracefully degrade if capabilities are unavailable
+
+## Common Workflows
+
+### Adding a New Volume Backend
+
+1. Create backend implementation in `app/server/modules/backends//`
+2. Implement `mount()` and `unmount()` methods
+3. Add schema to `app/schemas/volumes.ts`
+4. Update `volumeConfigSchema` discriminated union
+5. Update backend factory in `app/server/modules/backends/backend.ts`
+
+### Adding a New Repository Backend
+
+1. Add backend type to `app/schemas/restic.ts`
+2. Update `buildRepoUrl()` in `app/server/utils/restic.ts`
+3. Update `buildEnv()` to handle credentials/configuration
+4. Add DTO schemas in `app/server/modules/repositories/repositories.dto.ts`
+5. Update repository service to handle new backend
+
+### Adding a New Scheduled Job
+
+1. Create job class in `app/server/jobs/.ts` extending `Job`
+2. Implement `run()` method
+3. Register in `app/server/modules/lifecycle/startup.ts` with cron expression:
+ ```typescript
+ Scheduler.build(YourJob).schedule("* * * * *");
+ ```
+
+## Important Notes
+
+- **Code Style**: Uses Biome with tabs (not spaces), 120 char line width, double quotes
+- **Imports**: Organize imports is disabled in Biome - do not auto-organize
+- **TypeScript**: Uses `"type": "module"` - all imports must include extensions when targeting Node/Bun
+- **Validation**: Prefer ArkType over Zod - it's used throughout the codebase
+- **Database**: Timestamps are stored as Unix epoch integers, not ISO strings
+- **Security**: Restic password file has 0600 permissions - never expose it
+- **Mounting**: Requires privileged container or CAP_SYS_ADMIN for FUSE mounts
+- **API Documentation**: OpenAPI spec auto-generated at `/api/v1/openapi.json`, docs at `/api/v1/docs`
+
+## Docker Development Setup
+
+The `docker-compose.yml` defines two services:
+
+- `zerobyte-dev` - Development with hot reload (uses `development` stage)
+- `zerobyte-prod` - Production build (uses `production` stage)
+
+Both mount:
+
+- `/var/lib/zerobyte` for persistent data
+- `/dev/fuse` device for FUSE mounting
+- Optionally `/var/run/docker.sock` for Docker plugin functionality
diff --git a/Dockerfile b/Dockerfile
index 52676669..206d294b 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,8 +1,8 @@
-ARG BUN_VERSION="1.3.1"
+ARG BUN_VERSION="1.3.3"
FROM oven/bun:${BUN_VERSION}-alpine AS base
-RUN apk add --no-cache davfs2=1.6.1-r2 openssh-client
+RUN apk add --no-cache davfs2=1.6.1-r2 openssh-client fuse3
# ------------------------------
@@ -14,7 +14,7 @@ WORKDIR /deps
ARG TARGETARCH
ARG RESTIC_VERSION="0.18.1"
-ARG SHOUTRRR_VERSION="0.12.0"
+ARG SHOUTRRR_VERSION="0.12.1"
ENV TARGETARCH=${TARGETARCH}
RUN apk add --no-cache curl bzip2 unzip tar
diff --git a/README.md b/README.md
index f9838551..226a38f9 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.13
+ image: ghcr.io/nicotsx/zerobyte:v0.18
container_name: zerobyte
restart: unless-stopped
cap_add:
@@ -78,7 +78,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.13
+ image: ghcr.io/nicotsx/zerobyte:v0.18
container_name: zerobyte
restart: unless-stopped
cap_add:
@@ -146,7 +146,7 @@ Zerobyte can use [rclone](https://rclone.org/) to support 40+ cloud storage prov
```diff
services:
zerobyte:
- image: ghcr.io/nicotsx/zerobyte:v0.13
+ image: ghcr.io/nicotsx/zerobyte:v0.18
container_name: zerobyte
restart: unless-stopped
cap_add:
@@ -223,7 +223,7 @@ In order to enable this feature, you need to change your bind mount `/var/lib/ze
```diff
services:
zerobyte:
- image: ghcr.io/nicotsx/zerobyte:v0.13
+ image: ghcr.io/nicotsx/zerobyte:v0.18
container_name: zerobyte
restart: unless-stopped
ports:
@@ -254,7 +254,7 @@ In order to enable this feature, you need to run Zerobyte with several items sha
```diff
services:
zerobyte:
- image: ghcr.io/nicotsx/zerobyte:v0.13
+ image: ghcr.io/nicotsx/zerobyte:v0.18
container_name: zerobyte
restart: unless-stopped
cap_add:
diff --git a/app/client/api-client/@tanstack/react-query.gen.ts b/app/client/api-client/@tanstack/react-query.gen.ts
index fab5ce72..83ae179c 100644
--- a/app/client/api-client/@tanstack/react-query.gen.ts
+++ b/app/client/api-client/@tanstack/react-query.gen.ts
@@ -3,8 +3,8 @@
import { type DefaultError, queryOptions, type UseMutationOptions } from '@tanstack/react-query';
import { client } from '../client.gen';
-import { browseFilesystem, changePassword, createBackupSchedule, createNotificationDestination, createRepository, createVolume, deleteBackupSchedule, deleteNotificationDestination, deleteRepository, deleteSnapshot, deleteVolume, doctorRepository, downloadResticPassword, exportFullConfig, getBackupSchedule, getBackupScheduleForVolume, getContainersUsingVolume, getMe, getNotificationDestination, getRepository, getScheduleNotifications, getSnapshotDetails, getStatus, getSystemInfo, getVolume, healthCheckVolume, listBackupSchedules, listFiles, listNotificationDestinations, listRcloneRemotes, listRepositories, listSnapshotFiles, listSnapshots, listVolumes, login, logout, mountVolume, type Options, register, restoreSnapshot, runBackupNow, runForget, stopBackup, testConnection, testNotificationDestination, unmountVolume, updateBackupSchedule, updateNotificationDestination, updateRepository, updateScheduleNotifications, updateVolume } from '../sdk.gen';
-import type { BrowseFilesystemData, BrowseFilesystemResponse, ChangePasswordData, ChangePasswordResponse, CreateBackupScheduleData, CreateBackupScheduleResponse, CreateNotificationDestinationData, CreateNotificationDestinationResponse, CreateRepositoryData, CreateRepositoryResponse, CreateVolumeData, CreateVolumeResponse, DeleteBackupScheduleData, DeleteBackupScheduleResponse, DeleteNotificationDestinationData, DeleteNotificationDestinationResponse, DeleteRepositoryData, DeleteRepositoryResponse, DeleteSnapshotData, DeleteSnapshotResponse, DeleteVolumeData, DeleteVolumeResponse, DoctorRepositoryData, DoctorRepositoryResponse, DownloadResticPasswordData, DownloadResticPasswordResponse, ExportFullConfigData, ExportFullConfigError, ExportFullConfigResponse, GetBackupScheduleData, GetBackupScheduleForVolumeData, GetBackupScheduleForVolumeResponse, GetBackupScheduleResponse, GetContainersUsingVolumeData, GetContainersUsingVolumeResponse, GetMeData, GetMeResponse, GetNotificationDestinationData, GetNotificationDestinationResponse, GetRepositoryData, GetRepositoryResponse, GetScheduleNotificationsData, GetScheduleNotificationsResponse, GetSnapshotDetailsData, GetSnapshotDetailsResponse, GetStatusData, GetStatusResponse, GetSystemInfoData, GetSystemInfoResponse, GetVolumeData, GetVolumeResponse, HealthCheckVolumeData, HealthCheckVolumeResponse, ListBackupSchedulesData, ListBackupSchedulesResponse, ListFilesData, ListFilesResponse, ListNotificationDestinationsData, ListNotificationDestinationsResponse, ListRcloneRemotesData, ListRcloneRemotesResponse, ListRepositoriesData, ListRepositoriesResponse, ListSnapshotFilesData, ListSnapshotFilesResponse, ListSnapshotsData, ListSnapshotsResponse, ListVolumesData, ListVolumesResponse, LoginData, LoginResponse, LogoutData, LogoutResponse, MountVolumeData, MountVolumeResponse, RegisterData, RegisterResponse, RestoreSnapshotData, RestoreSnapshotResponse, RunBackupNowData, RunBackupNowResponse, RunForgetData, RunForgetResponse, StopBackupData, StopBackupResponse, TestConnectionData, TestConnectionResponse, TestNotificationDestinationData, TestNotificationDestinationResponse, UnmountVolumeData, UnmountVolumeResponse, UpdateBackupScheduleData, UpdateBackupScheduleResponse, UpdateNotificationDestinationData, UpdateNotificationDestinationResponse, UpdateRepositoryData, UpdateRepositoryResponse, UpdateScheduleNotificationsData, UpdateScheduleNotificationsResponse, UpdateVolumeData, UpdateVolumeResponse } from '../types.gen';
+import { browseFilesystem, changePassword, createBackupSchedule, createNotificationDestination, createRepository, createVolume, deleteBackupSchedule, deleteNotificationDestination, deleteRepository, deleteSnapshot, deleteVolume, doctorRepository, downloadResticPassword, exportFullConfig, getBackupSchedule, getBackupScheduleForVolume, getContainersUsingVolume, getMe, getMirrorCompatibility, getNotificationDestination, getRepository, getScheduleMirrors, getScheduleNotifications, getSnapshotDetails, getStatus, getSystemInfo, getVolume, healthCheckVolume, listBackupSchedules, listFiles, listNotificationDestinations, listRcloneRemotes, listRepositories, listSnapshotFiles, listSnapshots, listVolumes, login, logout, mountVolume, type Options, register, restoreSnapshot, runBackupNow, runForget, stopBackup, testConnection, testNotificationDestination, unmountVolume, updateBackupSchedule, updateNotificationDestination, updateRepository, updateScheduleMirrors, updateScheduleNotifications, updateVolume } from '../sdk.gen';
+import type { BrowseFilesystemData, BrowseFilesystemResponse, ChangePasswordData, ChangePasswordResponse, CreateBackupScheduleData, CreateBackupScheduleResponse, CreateNotificationDestinationData, CreateNotificationDestinationResponse, CreateRepositoryData, CreateRepositoryResponse, CreateVolumeData, CreateVolumeResponse, DeleteBackupScheduleData, DeleteBackupScheduleResponse, DeleteNotificationDestinationData, DeleteNotificationDestinationResponse, DeleteRepositoryData, DeleteRepositoryResponse, DeleteSnapshotData, DeleteSnapshotResponse, DeleteVolumeData, DeleteVolumeResponse, DoctorRepositoryData, DoctorRepositoryResponse, DownloadResticPasswordData, DownloadResticPasswordResponse, ExportFullConfigData, ExportFullConfigError, ExportFullConfigResponse, GetBackupScheduleData, GetBackupScheduleForVolumeData, GetBackupScheduleForVolumeResponse, GetBackupScheduleResponse, GetContainersUsingVolumeData, GetContainersUsingVolumeResponse, GetMeData, GetMeResponse, GetMirrorCompatibilityData, GetMirrorCompatibilityResponse, GetNotificationDestinationData, GetNotificationDestinationResponse, GetRepositoryData, GetRepositoryResponse, GetScheduleMirrorsData, GetScheduleMirrorsResponse, GetScheduleNotificationsData, GetScheduleNotificationsResponse, GetSnapshotDetailsData, GetSnapshotDetailsResponse, GetStatusData, GetStatusResponse, GetSystemInfoData, GetSystemInfoResponse, GetVolumeData, GetVolumeResponse, HealthCheckVolumeData, HealthCheckVolumeResponse, ListBackupSchedulesData, ListBackupSchedulesResponse, ListFilesData, ListFilesResponse, ListNotificationDestinationsData, ListNotificationDestinationsResponse, ListRcloneRemotesData, ListRcloneRemotesResponse, ListRepositoriesData, ListRepositoriesResponse, ListSnapshotFilesData, ListSnapshotFilesResponse, ListSnapshotsData, ListSnapshotsResponse, ListVolumesData, ListVolumesResponse, LoginData, LoginResponse, LogoutData, LogoutResponse, MountVolumeData, MountVolumeResponse, RegisterData, RegisterResponse, RestoreSnapshotData, RestoreSnapshotResponse, RunBackupNowData, RunBackupNowResponse, RunForgetData, RunForgetResponse, StopBackupData, StopBackupResponse, TestConnectionData, TestConnectionResponse, TestNotificationDestinationData, TestNotificationDestinationResponse, UnmountVolumeData, UnmountVolumeResponse, UpdateBackupScheduleData, UpdateBackupScheduleResponse, UpdateNotificationDestinationData, UpdateNotificationDestinationResponse, UpdateRepositoryData, UpdateRepositoryResponse, UpdateScheduleMirrorsData, UpdateScheduleMirrorsResponse, UpdateScheduleNotificationsData, UpdateScheduleNotificationsResponse, UpdateVolumeData, UpdateVolumeResponse } from '../types.gen';
/**
* Register a new user
@@ -755,6 +755,59 @@ export const updateScheduleNotificationsMutation = (options?: Partial) => createQueryKey("getScheduleMirrors", options);
+
+/**
+ * Get mirror repository assignments for a backup schedule
+ */
+export const getScheduleMirrorsOptions = (options: Options) => queryOptions>({
+ queryFn: async ({ queryKey, signal }) => {
+ const { data } = await getScheduleMirrors({
+ ...options,
+ ...queryKey[0],
+ signal,
+ throwOnError: true
+ });
+ return data;
+ },
+ queryKey: getScheduleMirrorsQueryKey(options)
+});
+
+/**
+ * Update mirror repository assignments for a backup schedule
+ */
+export const updateScheduleMirrorsMutation = (options?: Partial>): UseMutationOptions> => {
+ const mutationOptions: UseMutationOptions> = {
+ mutationFn: async (fnOptions) => {
+ const { data } = await updateScheduleMirrors({
+ ...options,
+ ...fnOptions,
+ throwOnError: true
+ });
+ return data;
+ }
+ };
+ return mutationOptions;
+};
+
+export const getMirrorCompatibilityQueryKey = (options: Options) => createQueryKey("getMirrorCompatibility", options);
+
+/**
+ * Get mirror compatibility info for all repositories relative to a backup schedule's primary repository
+ */
+export const getMirrorCompatibilityOptions = (options: Options) => queryOptions>({
+ queryFn: async ({ queryKey, signal }) => {
+ const { data } = await getMirrorCompatibility({
+ ...options,
+ ...queryKey[0],
+ signal,
+ throwOnError: true
+ });
+ return data;
+ },
+ queryKey: getMirrorCompatibilityQueryKey(options)
+});
+
export const listNotificationDestinationsQueryKey = (options?: Options) => createQueryKey("listNotificationDestinations", options);
/**
diff --git a/app/client/api-client/sdk.gen.ts b/app/client/api-client/sdk.gen.ts
index 7778eb87..6d83b761 100644
--- a/app/client/api-client/sdk.gen.ts
+++ b/app/client/api-client/sdk.gen.ts
@@ -2,7 +2,7 @@
import type { Client, Options as Options2, TDataShape } from './client';
import { client } from './client.gen';
-import type { BrowseFilesystemData, BrowseFilesystemResponses, ChangePasswordData, ChangePasswordResponses, CreateBackupScheduleData, CreateBackupScheduleResponses, CreateNotificationDestinationData, CreateNotificationDestinationResponses, CreateRepositoryData, CreateRepositoryResponses, CreateVolumeData, CreateVolumeResponses, DeleteBackupScheduleData, DeleteBackupScheduleResponses, DeleteNotificationDestinationData, DeleteNotificationDestinationErrors, DeleteNotificationDestinationResponses, DeleteRepositoryData, DeleteRepositoryResponses, DeleteSnapshotData, DeleteSnapshotResponses, DeleteVolumeData, DeleteVolumeResponses, DoctorRepositoryData, DoctorRepositoryResponses, DownloadResticPasswordData, DownloadResticPasswordResponses, ExportFullConfigData, ExportFullConfigErrors, ExportFullConfigResponses, GetBackupScheduleData, GetBackupScheduleForVolumeData, GetBackupScheduleForVolumeResponses, GetBackupScheduleResponses, GetContainersUsingVolumeData, GetContainersUsingVolumeErrors, GetContainersUsingVolumeResponses, GetMeData, GetMeResponses, GetNotificationDestinationData, GetNotificationDestinationErrors, GetNotificationDestinationResponses, GetRepositoryData, GetRepositoryResponses, GetScheduleNotificationsData, GetScheduleNotificationsResponses, GetSnapshotDetailsData, GetSnapshotDetailsResponses, GetStatusData, GetStatusResponses, GetSystemInfoData, GetSystemInfoResponses, GetVolumeData, GetVolumeErrors, GetVolumeResponses, HealthCheckVolumeData, HealthCheckVolumeErrors, HealthCheckVolumeResponses, ListBackupSchedulesData, ListBackupSchedulesResponses, ListFilesData, ListFilesResponses, ListNotificationDestinationsData, ListNotificationDestinationsResponses, ListRcloneRemotesData, ListRcloneRemotesResponses, ListRepositoriesData, ListRepositoriesResponses, ListSnapshotFilesData, ListSnapshotFilesResponses, ListSnapshotsData, ListSnapshotsResponses, ListVolumesData, ListVolumesResponses, LoginData, LoginResponses, LogoutData, LogoutResponses, MountVolumeData, MountVolumeResponses, RegisterData, RegisterResponses, RestoreSnapshotData, RestoreSnapshotResponses, RunBackupNowData, RunBackupNowResponses, RunForgetData, RunForgetResponses, StopBackupData, StopBackupErrors, StopBackupResponses, TestConnectionData, TestConnectionResponses, TestNotificationDestinationData, TestNotificationDestinationErrors, TestNotificationDestinationResponses, UnmountVolumeData, UnmountVolumeResponses, UpdateBackupScheduleData, UpdateBackupScheduleResponses, UpdateNotificationDestinationData, UpdateNotificationDestinationErrors, UpdateNotificationDestinationResponses, UpdateRepositoryData, UpdateRepositoryErrors, UpdateRepositoryResponses, UpdateScheduleNotificationsData, UpdateScheduleNotificationsResponses, UpdateVolumeData, UpdateVolumeErrors, UpdateVolumeResponses } from './types.gen';
+import type { BrowseFilesystemData, BrowseFilesystemResponses, ChangePasswordData, ChangePasswordResponses, CreateBackupScheduleData, CreateBackupScheduleResponses, CreateNotificationDestinationData, CreateNotificationDestinationResponses, CreateRepositoryData, CreateRepositoryResponses, CreateVolumeData, CreateVolumeResponses, DeleteBackupScheduleData, DeleteBackupScheduleResponses, DeleteNotificationDestinationData, DeleteNotificationDestinationErrors, DeleteNotificationDestinationResponses, DeleteRepositoryData, DeleteRepositoryResponses, DeleteSnapshotData, DeleteSnapshotResponses, DeleteVolumeData, DeleteVolumeResponses, DoctorRepositoryData, DoctorRepositoryResponses, DownloadResticPasswordData, DownloadResticPasswordResponses, ExportFullConfigData, ExportFullConfigErrors, ExportFullConfigResponses, GetBackupScheduleData, GetBackupScheduleForVolumeData, GetBackupScheduleForVolumeResponses, GetBackupScheduleResponses, GetContainersUsingVolumeData, GetContainersUsingVolumeErrors, GetContainersUsingVolumeResponses, GetMeData, GetMeResponses, GetMirrorCompatibilityData, GetMirrorCompatibilityResponses, GetNotificationDestinationData, GetNotificationDestinationErrors, GetNotificationDestinationResponses, GetRepositoryData, GetRepositoryResponses, GetScheduleMirrorsData, GetScheduleMirrorsResponses, GetScheduleNotificationsData, GetScheduleNotificationsResponses, GetSnapshotDetailsData, GetSnapshotDetailsResponses, GetStatusData, GetStatusResponses, GetSystemInfoData, GetSystemInfoResponses, GetVolumeData, GetVolumeErrors, GetVolumeResponses, HealthCheckVolumeData, HealthCheckVolumeErrors, HealthCheckVolumeResponses, ListBackupSchedulesData, ListBackupSchedulesResponses, ListFilesData, ListFilesResponses, ListNotificationDestinationsData, ListNotificationDestinationsResponses, ListRcloneRemotesData, ListRcloneRemotesResponses, ListRepositoriesData, ListRepositoriesResponses, ListSnapshotFilesData, ListSnapshotFilesResponses, ListSnapshotsData, ListSnapshotsResponses, ListVolumesData, ListVolumesResponses, LoginData, LoginResponses, LogoutData, LogoutResponses, MountVolumeData, MountVolumeResponses, RegisterData, RegisterResponses, RestoreSnapshotData, RestoreSnapshotResponses, RunBackupNowData, RunBackupNowResponses, RunForgetData, RunForgetResponses, StopBackupData, StopBackupErrors, StopBackupResponses, TestConnectionData, TestConnectionResponses, TestNotificationDestinationData, TestNotificationDestinationErrors, TestNotificationDestinationResponses, UnmountVolumeData, UnmountVolumeResponses, UpdateBackupScheduleData, UpdateBackupScheduleResponses, UpdateNotificationDestinationData, UpdateNotificationDestinationErrors, UpdateNotificationDestinationResponses, UpdateRepositoryData, UpdateRepositoryErrors, UpdateRepositoryResponses, UpdateScheduleMirrorsData, UpdateScheduleMirrorsResponses, UpdateScheduleNotificationsData, UpdateScheduleNotificationsResponses, UpdateVolumeData, UpdateVolumeErrors, UpdateVolumeResponses } from './types.gen';
export type Options = Options2 & {
/**
@@ -476,6 +476,40 @@ export const updateScheduleNotifications = (options: Options) => {
+ return (options.client ?? client).get({
+ url: '/api/v1/backups/{scheduleId}/mirrors',
+ ...options
+ });
+};
+
+/**
+ * Update mirror repository assignments for a backup schedule
+ */
+export const updateScheduleMirrors = (options: Options) => {
+ return (options.client ?? client).put({
+ url: '/api/v1/backups/{scheduleId}/mirrors',
+ ...options,
+ headers: {
+ 'Content-Type': 'application/json',
+ ...options.headers
+ }
+ });
+};
+
+/**
+ * Get mirror compatibility info for all repositories relative to a backup schedule's primary repository
+ */
+export const getMirrorCompatibility = (options: Options) => {
+ return (options.client ?? client).get({
+ url: '/api/v1/backups/{scheduleId}/mirrors/compatibility',
+ ...options
+ });
+};
+
/**
* List all notification destinations
*/
diff --git a/app/client/api-client/types.gen.ts b/app/client/api-client/types.gen.ts
index d196feef..3b0fb9bb 100644
--- a/app/client/api-client/types.gen.ts
+++ b/app/client/api-client/types.gen.ts
@@ -164,6 +164,11 @@ export type ListVolumesResponses = {
version: '3' | '4' | '4.1';
port?: number;
readOnly?: boolean;
+ } | {
+ backend: 'rclone';
+ path: string;
+ remote: string;
+ readOnly?: boolean;
} | {
backend: 'smb';
password: string;
@@ -191,7 +196,7 @@ export type ListVolumesResponses = {
name: string;
shortId: string;
status: 'error' | 'mounted' | 'unmounted';
- type: 'directory' | 'nfs' | 'smb' | 'webdav';
+ type: 'directory' | 'nfs' | 'rclone' | 'smb' | 'webdav';
updatedAt: number;
}>;
};
@@ -211,6 +216,11 @@ export type CreateVolumeData = {
version: '3' | '4' | '4.1';
port?: number;
readOnly?: boolean;
+ } | {
+ backend: 'rclone';
+ path: string;
+ remote: string;
+ readOnly?: boolean;
} | {
backend: 'smb';
password: string;
@@ -255,6 +265,11 @@ export type CreateVolumeResponses = {
version: '3' | '4' | '4.1';
port?: number;
readOnly?: boolean;
+ } | {
+ backend: 'rclone';
+ path: string;
+ remote: string;
+ readOnly?: boolean;
} | {
backend: 'smb';
password: string;
@@ -282,7 +297,7 @@ export type CreateVolumeResponses = {
name: string;
shortId: string;
status: 'error' | 'mounted' | 'unmounted';
- type: 'directory' | 'nfs' | 'smb' | 'webdav';
+ type: 'directory' | 'nfs' | 'rclone' | 'smb' | 'webdav';
updatedAt: number;
};
};
@@ -302,6 +317,11 @@ export type TestConnectionData = {
version: '3' | '4' | '4.1';
port?: number;
readOnly?: boolean;
+ } | {
+ backend: 'rclone';
+ path: string;
+ remote: string;
+ readOnly?: boolean;
} | {
backend: 'smb';
password: string;
@@ -399,6 +419,11 @@ export type GetVolumeResponses = {
version: '3' | '4' | '4.1';
port?: number;
readOnly?: boolean;
+ } | {
+ backend: 'rclone';
+ path: string;
+ remote: string;
+ readOnly?: boolean;
} | {
backend: 'smb';
password: string;
@@ -426,7 +451,7 @@ export type GetVolumeResponses = {
name: string;
shortId: string;
status: 'error' | 'mounted' | 'unmounted';
- type: 'directory' | 'nfs' | 'smb' | 'webdav';
+ type: 'directory' | 'nfs' | 'rclone' | 'smb' | 'webdav';
updatedAt: number;
};
};
@@ -448,6 +473,11 @@ export type UpdateVolumeData = {
version: '3' | '4' | '4.1';
port?: number;
readOnly?: boolean;
+ } | {
+ backend: 'rclone';
+ path: string;
+ remote: string;
+ readOnly?: boolean;
} | {
backend: 'smb';
password: string;
@@ -501,6 +531,11 @@ export type UpdateVolumeResponses = {
version: '3' | '4' | '4.1';
port?: number;
readOnly?: boolean;
+ } | {
+ backend: 'rclone';
+ path: string;
+ remote: string;
+ readOnly?: boolean;
} | {
backend: 'smb';
password: string;
@@ -528,7 +563,7 @@ export type UpdateVolumeResponses = {
name: string;
shortId: string;
status: 'error' | 'mounted' | 'unmounted';
- type: 'directory' | 'nfs' | 'smb' | 'webdav';
+ type: 'directory' | 'nfs' | 'rclone' | 'smb' | 'webdav';
updatedAt: number;
};
};
@@ -1124,6 +1159,7 @@ export type ListSnapshotsResponses = {
paths: Array;
short_id: string;
size: number;
+ tags: Array;
time: number;
}>;
};
@@ -1170,6 +1206,7 @@ export type GetSnapshotDetailsResponses = {
paths: Array;
short_id: string;
size: number;
+ tags: Array;
time: number;
};
};
@@ -1289,12 +1326,14 @@ export type ListBackupSchedulesResponses = {
createdAt: number;
cronExpression: string;
enabled: boolean;
+ excludeIfPresent: Array | null;
excludePatterns: Array | null;
id: number;
includePatterns: Array | null;
lastBackupAt: number | null;
lastBackupError: string | null;
lastBackupStatus: 'error' | 'in_progress' | 'success' | 'warning' | null;
+ name: string;
nextBackupAt: number | null;
repository: {
compressionMode: 'auto' | 'max' | 'off' | null;
@@ -1393,6 +1432,11 @@ export type ListBackupSchedulesResponses = {
version: '3' | '4' | '4.1';
port?: number;
readOnly?: boolean;
+ } | {
+ backend: 'rclone';
+ path: string;
+ remote: string;
+ readOnly?: boolean;
} | {
backend: 'smb';
password: string;
@@ -1420,7 +1464,7 @@ export type ListBackupSchedulesResponses = {
name: string;
shortId: string;
status: 'error' | 'mounted' | 'unmounted';
- type: 'directory' | 'nfs' | 'smb' | 'webdav';
+ type: 'directory' | 'nfs' | 'rclone' | 'smb' | 'webdav';
updatedAt: number;
};
volumeId: number;
@@ -1433,8 +1477,10 @@ export type CreateBackupScheduleData = {
body?: {
cronExpression: string;
enabled: boolean;
+ name: string;
repositoryId: string;
volumeId: number;
+ excludeIfPresent?: Array;
excludePatterns?: Array;
includePatterns?: Array;
retentionPolicy?: {
@@ -1461,12 +1507,14 @@ export type CreateBackupScheduleResponses = {
createdAt: number;
cronExpression: string;
enabled: boolean;
+ excludeIfPresent: Array | null;
excludePatterns: Array | null;
id: number;
includePatterns: Array | null;
lastBackupAt: number | null;
lastBackupError: string | null;
lastBackupStatus: 'error' | 'in_progress' | 'success' | 'warning' | null;
+ name: string;
nextBackupAt: number | null;
repositoryId: string;
retentionPolicy: {
@@ -1522,12 +1570,14 @@ export type GetBackupScheduleResponses = {
createdAt: number;
cronExpression: string;
enabled: boolean;
+ excludeIfPresent: Array | null;
excludePatterns: Array | null;
id: number;
includePatterns: Array | null;
lastBackupAt: number | null;
lastBackupError: string | null;
lastBackupStatus: 'error' | 'in_progress' | 'success' | 'warning' | null;
+ name: string;
nextBackupAt: number | null;
repository: {
compressionMode: 'auto' | 'max' | 'off' | null;
@@ -1626,6 +1676,11 @@ export type GetBackupScheduleResponses = {
version: '3' | '4' | '4.1';
port?: number;
readOnly?: boolean;
+ } | {
+ backend: 'rclone';
+ path: string;
+ remote: string;
+ readOnly?: boolean;
} | {
backend: 'smb';
password: string;
@@ -1653,7 +1708,7 @@ export type GetBackupScheduleResponses = {
name: string;
shortId: string;
status: 'error' | 'mounted' | 'unmounted';
- type: 'directory' | 'nfs' | 'smb' | 'webdav';
+ type: 'directory' | 'nfs' | 'rclone' | 'smb' | 'webdav';
updatedAt: number;
};
volumeId: number;
@@ -1667,8 +1722,10 @@ export type UpdateBackupScheduleData = {
cronExpression: string;
repositoryId: string;
enabled?: boolean;
+ excludeIfPresent?: Array;
excludePatterns?: Array;
includePatterns?: Array;
+ name?: string;
retentionPolicy?: {
keepDaily?: number;
keepHourly?: number;
@@ -1695,12 +1752,14 @@ export type UpdateBackupScheduleResponses = {
createdAt: number;
cronExpression: string;
enabled: boolean;
+ excludeIfPresent: Array | null;
excludePatterns: Array | null;
id: number;
includePatterns: Array | null;
lastBackupAt: number | null;
lastBackupError: string | null;
lastBackupStatus: 'error' | 'in_progress' | 'success' | 'warning' | null;
+ name: string;
nextBackupAt: number | null;
repositoryId: string;
retentionPolicy: {
@@ -1736,12 +1795,14 @@ export type GetBackupScheduleForVolumeResponses = {
createdAt: number;
cronExpression: string;
enabled: boolean;
+ excludeIfPresent: Array | null;
excludePatterns: Array | null;
id: number;
includePatterns: Array | null;
lastBackupAt: number | null;
lastBackupError: string | null;
lastBackupStatus: 'error' | 'in_progress' | 'success' | 'warning' | null;
+ name: string;
nextBackupAt: number | null;
repository: {
compressionMode: 'auto' | 'max' | 'off' | null;
@@ -1840,6 +1901,11 @@ export type GetBackupScheduleForVolumeResponses = {
version: '3' | '4' | '4.1';
port?: number;
readOnly?: boolean;
+ } | {
+ backend: 'rclone';
+ path: string;
+ remote: string;
+ readOnly?: boolean;
} | {
backend: 'smb';
password: string;
@@ -1867,7 +1933,7 @@ export type GetBackupScheduleForVolumeResponses = {
name: string;
shortId: string;
status: 'error' | 'mounted' | 'unmounted';
- type: 'directory' | 'nfs' | 'smb' | 'webdav';
+ type: 'directory' | 'nfs' | 'rclone' | 'smb' | 'webdav';
updatedAt: number;
};
volumeId: number;
@@ -1971,13 +2037,13 @@ export type GetScheduleNotificationsResponses = {
type: 'telegram';
} | {
from: string;
- password: string;
smtpHost: string;
smtpPort: number;
to: Array;
type: 'email';
useTLS: boolean;
- username: string;
+ password?: string;
+ username?: string;
} | {
priority: 'default' | 'high' | 'low' | 'max' | 'min';
topic: string;
@@ -2018,6 +2084,7 @@ export type GetScheduleNotificationsResponses = {
notifyOnFailure: boolean;
notifyOnStart: boolean;
notifyOnSuccess: boolean;
+ notifyOnWarning: boolean;
scheduleId: number;
}>;
};
@@ -2031,6 +2098,7 @@ export type UpdateScheduleNotificationsData = {
notifyOnFailure: boolean;
notifyOnStart: boolean;
notifyOnSuccess: boolean;
+ notifyOnWarning: boolean;
}>;
};
path: {
@@ -2059,13 +2127,13 @@ export type UpdateScheduleNotificationsResponses = {
type: 'telegram';
} | {
from: string;
- password: string;
smtpHost: string;
smtpPort: number;
to: Array;
type: 'email';
useTLS: boolean;
- username: string;
+ password?: string;
+ username?: string;
} | {
priority: 'default' | 'high' | 'low' | 'max' | 'min';
topic: string;
@@ -2106,12 +2174,238 @@ export type UpdateScheduleNotificationsResponses = {
notifyOnFailure: boolean;
notifyOnStart: boolean;
notifyOnSuccess: boolean;
+ notifyOnWarning: boolean;
scheduleId: number;
}>;
};
export type UpdateScheduleNotificationsResponse = UpdateScheduleNotificationsResponses[keyof UpdateScheduleNotificationsResponses];
+export type GetScheduleMirrorsData = {
+ body?: never;
+ path: {
+ scheduleId: string;
+ };
+ query?: never;
+ url: '/api/v1/backups/{scheduleId}/mirrors';
+};
+
+export type GetScheduleMirrorsResponses = {
+ /**
+ * List of mirror repository assignments for the schedule
+ */
+ 200: Array<{
+ createdAt: number;
+ enabled: boolean;
+ lastCopyAt: number | null;
+ lastCopyError: string | null;
+ lastCopyStatus: 'error' | 'success' | null;
+ repository: {
+ compressionMode: 'auto' | 'max' | 'off' | null;
+ config: {
+ accessKeyId: string;
+ backend: 'r2';
+ bucket: string;
+ endpoint: string;
+ secretAccessKey: string;
+ customPassword?: string;
+ isExistingRepository?: boolean;
+ } | {
+ accessKeyId: string;
+ backend: 's3';
+ bucket: string;
+ endpoint: string;
+ secretAccessKey: string;
+ customPassword?: string;
+ isExistingRepository?: boolean;
+ } | {
+ accountKey: string;
+ accountName: string;
+ backend: 'azure';
+ container: string;
+ customPassword?: string;
+ endpointSuffix?: string;
+ isExistingRepository?: boolean;
+ } | {
+ backend: 'gcs';
+ bucket: string;
+ credentialsJson: string;
+ projectId: string;
+ customPassword?: string;
+ isExistingRepository?: boolean;
+ } | {
+ backend: 'local';
+ name: string;
+ customPassword?: string;
+ isExistingRepository?: boolean;
+ path?: string;
+ } | {
+ backend: 'rclone';
+ path: string;
+ remote: string;
+ customPassword?: string;
+ isExistingRepository?: boolean;
+ } | {
+ backend: 'rest';
+ url: string;
+ customPassword?: string;
+ isExistingRepository?: boolean;
+ password?: string;
+ path?: string;
+ username?: string;
+ } | {
+ backend: 'sftp';
+ host: string;
+ path: string;
+ privateKey: string;
+ user: string;
+ port?: number;
+ customPassword?: string;
+ isExistingRepository?: boolean;
+ };
+ createdAt: number;
+ id: string;
+ lastChecked: number | null;
+ lastError: string | null;
+ name: string;
+ shortId: string;
+ status: 'error' | 'healthy' | 'unknown' | null;
+ type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 'rest' | 's3' | 'sftp';
+ updatedAt: number;
+ };
+ repositoryId: string;
+ scheduleId: number;
+ }>;
+};
+
+export type GetScheduleMirrorsResponse = GetScheduleMirrorsResponses[keyof GetScheduleMirrorsResponses];
+
+export type UpdateScheduleMirrorsData = {
+ body?: {
+ mirrors: Array<{
+ enabled: boolean;
+ repositoryId: string;
+ }>;
+ };
+ path: {
+ scheduleId: string;
+ };
+ query?: never;
+ url: '/api/v1/backups/{scheduleId}/mirrors';
+};
+
+export type UpdateScheduleMirrorsResponses = {
+ /**
+ * Mirror assignments updated successfully
+ */
+ 200: Array<{
+ createdAt: number;
+ enabled: boolean;
+ lastCopyAt: number | null;
+ lastCopyError: string | null;
+ lastCopyStatus: 'error' | 'success' | null;
+ repository: {
+ compressionMode: 'auto' | 'max' | 'off' | null;
+ config: {
+ accessKeyId: string;
+ backend: 'r2';
+ bucket: string;
+ endpoint: string;
+ secretAccessKey: string;
+ customPassword?: string;
+ isExistingRepository?: boolean;
+ } | {
+ accessKeyId: string;
+ backend: 's3';
+ bucket: string;
+ endpoint: string;
+ secretAccessKey: string;
+ customPassword?: string;
+ isExistingRepository?: boolean;
+ } | {
+ accountKey: string;
+ accountName: string;
+ backend: 'azure';
+ container: string;
+ customPassword?: string;
+ endpointSuffix?: string;
+ isExistingRepository?: boolean;
+ } | {
+ backend: 'gcs';
+ bucket: string;
+ credentialsJson: string;
+ projectId: string;
+ customPassword?: string;
+ isExistingRepository?: boolean;
+ } | {
+ backend: 'local';
+ name: string;
+ customPassword?: string;
+ isExistingRepository?: boolean;
+ path?: string;
+ } | {
+ backend: 'rclone';
+ path: string;
+ remote: string;
+ customPassword?: string;
+ isExistingRepository?: boolean;
+ } | {
+ backend: 'rest';
+ url: string;
+ customPassword?: string;
+ isExistingRepository?: boolean;
+ password?: string;
+ path?: string;
+ username?: string;
+ } | {
+ backend: 'sftp';
+ host: string;
+ path: string;
+ privateKey: string;
+ user: string;
+ port?: number;
+ customPassword?: string;
+ isExistingRepository?: boolean;
+ };
+ createdAt: number;
+ id: string;
+ lastChecked: number | null;
+ lastError: string | null;
+ name: string;
+ shortId: string;
+ status: 'error' | 'healthy' | 'unknown' | null;
+ type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 'rest' | 's3' | 'sftp';
+ updatedAt: number;
+ };
+ repositoryId: string;
+ scheduleId: number;
+ }>;
+};
+
+export type UpdateScheduleMirrorsResponse = UpdateScheduleMirrorsResponses[keyof UpdateScheduleMirrorsResponses];
+
+export type GetMirrorCompatibilityData = {
+ body?: never;
+ path: {
+ scheduleId: string;
+ };
+ query?: never;
+ url: '/api/v1/backups/{scheduleId}/mirrors/compatibility';
+};
+
+export type GetMirrorCompatibilityResponses = {
+ /**
+ * List of repositories with their mirror compatibility status
+ */
+ 200: Array<{
+ compatible: boolean;
+ reason: string | null;
+ repositoryId: string;
+ }>;
+};
+
+export type GetMirrorCompatibilityResponse = GetMirrorCompatibilityResponses[keyof GetMirrorCompatibilityResponses];
+
export type ListNotificationDestinationsData = {
body?: never;
path?: never;
@@ -2136,13 +2430,13 @@ export type ListNotificationDestinationsResponses = {
type: 'telegram';
} | {
from: string;
- password: string;
smtpHost: string;
smtpPort: number;
to: Array;
type: 'email';
useTLS: boolean;
- username: string;
+ password?: string;
+ username?: string;
} | {
priority: 'default' | 'high' | 'low' | 'max' | 'min';
topic: string;
@@ -2197,13 +2491,13 @@ export type CreateNotificationDestinationData = {
type: 'telegram';
} | {
from: string;
- password: string;
smtpHost: string;
smtpPort: number;
to: Array;
type: 'email';
useTLS: boolean;
- username: string;
+ password?: string;
+ username?: string;
} | {
priority: 'default' | 'high' | 'low' | 'max' | 'min';
topic: string;
@@ -2257,13 +2551,13 @@ export type CreateNotificationDestinationResponses = {
type: 'telegram';
} | {
from: string;
- password: string;
smtpHost: string;
smtpPort: number;
to: Array;
type: 'email';
useTLS: boolean;
- username: string;
+ password?: string;
+ username?: string;
} | {
priority: 'default' | 'high' | 'low' | 'max' | 'min';
topic: string;
@@ -2364,13 +2658,13 @@ export type GetNotificationDestinationResponses = {
type: 'telegram';
} | {
from: string;
- password: string;
smtpHost: string;
smtpPort: number;
to: Array;
type: 'email';
useTLS: boolean;
- username: string;
+ password?: string;
+ username?: string;
} | {
priority: 'default' | 'high' | 'low' | 'max' | 'min';
topic: string;
@@ -2425,13 +2719,13 @@ export type UpdateNotificationDestinationData = {
type: 'telegram';
} | {
from: string;
- password: string;
smtpHost: string;
smtpPort: number;
to: Array;
type: 'email';
useTLS: boolean;
- username: string;
+ password?: string;
+ username?: string;
} | {
priority: 'default' | 'high' | 'low' | 'max' | 'min';
topic: string;
@@ -2495,13 +2789,13 @@ export type UpdateNotificationDestinationResponses = {
type: 'telegram';
} | {
from: string;
- password: string;
smtpHost: string;
smtpPort: number;
to: Array;
type: 'email';
useTLS: boolean;
- username: string;
+ password?: string;
+ username?: string;
} | {
priority: 'default' | 'high' | 'low' | 'max' | 'min';
topic: string;
diff --git a/app/client/components/create-repository-form.tsx b/app/client/components/create-repository-form.tsx
deleted file mode 100644
index 255dd7a1..00000000
--- a/app/client/components/create-repository-form.tsx
+++ /dev/null
@@ -1,784 +0,0 @@
-import { arktypeResolver } from "@hookform/resolvers/arktype";
-import { type } from "arktype";
-import { useEffect, useState } from "react";
-import { useForm } from "react-hook-form";
-import { cn, slugify } from "~/client/lib/utils";
-import { deepClean } from "~/utils/object";
-import { Button } from "./ui/button";
-import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "./ui/form";
-import { Input } from "./ui/input";
-import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
-import { useQuery } from "@tanstack/react-query";
-import { Alert, AlertDescription } from "./ui/alert";
-import { ExternalLink, AlertTriangle } from "lucide-react";
-import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
-import { useSystemInfo } from "~/client/hooks/use-system-info";
-import { COMPRESSION_MODES, repositoryConfigSchema } from "~/schemas/restic";
-import { listRcloneRemotesOptions } from "../api-client/@tanstack/react-query.gen";
-import { Checkbox } from "./ui/checkbox";
-import { DirectoryBrowser } from "./directory-browser";
-import {
- AlertDialog,
- AlertDialogAction,
- AlertDialogCancel,
- AlertDialogContent,
- AlertDialogDescription,
- AlertDialogFooter,
- AlertDialogHeader,
- AlertDialogTitle,
-} from "./ui/alert-dialog";
-import { Textarea } from "./ui/textarea";
-
-export const formSchema = type({
- name: "2<=string<=32",
- compressionMode: type.valueOf(COMPRESSION_MODES).optional(),
-}).and(repositoryConfigSchema);
-const cleanSchema = type.pipe((d) => formSchema(deepClean(d)));
-
-export type RepositoryFormValues = typeof formSchema.inferIn;
-
-type Props = {
- onSubmit: (values: RepositoryFormValues) => void;
- mode?: "create" | "update";
- initialValues?: Partial;
- formId?: string;
- loading?: boolean;
- className?: string;
-};
-
-const defaultValuesForType = {
- local: { backend: "local" as const, compressionMode: "auto" as const },
- s3: { backend: "s3" as const, compressionMode: "auto" as const },
- r2: { backend: "r2" as const, compressionMode: "auto" as const },
- gcs: { backend: "gcs" as const, compressionMode: "auto" as const },
- azure: { backend: "azure" as const, compressionMode: "auto" as const },
- rclone: { backend: "rclone" as const, compressionMode: "auto" as const },
- rest: { backend: "rest" as const, compressionMode: "auto" as const },
- sftp: { backend: "sftp" as const, compressionMode: "auto" as const, port: 22 },
-};
-
-export const CreateRepositoryForm = ({
- onSubmit,
- mode = "create",
- initialValues,
- formId,
- loading,
- className,
-}: Props) => {
- const form = useForm({
- resolver: arktypeResolver(cleanSchema as unknown as typeof formSchema),
- defaultValues: initialValues,
- resetOptions: {
- keepDefaultValues: true,
- keepDirtyValues: false,
- },
- });
-
- const { watch, setValue } = form;
-
- const watchedBackend = watch("backend");
- const watchedIsExistingRepository = watch("isExistingRepository");
-
- const [passwordMode, setPasswordMode] = useState<"default" | "custom">("default");
- const [showPathBrowser, setShowPathBrowser] = useState(false);
- const [showPathWarning, setShowPathWarning] = useState(false);
-
- const { capabilities } = useSystemInfo();
-
- const { data: rcloneRemotes, isLoading: isLoadingRemotes } = useQuery({
- ...listRcloneRemotesOptions(),
- enabled: capabilities.rclone,
- });
-
- useEffect(() => {
- form.reset({
- name: form.getValues().name,
- isExistingRepository: form.getValues().isExistingRepository,
- customPassword: form.getValues().customPassword,
- ...defaultValuesForType[watchedBackend as keyof typeof defaultValuesForType],
- });
- }, [watchedBackend, form]);
-
- return (
-
-
- );
-};
diff --git a/app/client/components/create-volume-form.tsx b/app/client/components/create-volume-form.tsx
deleted file mode 100644
index a4fa6920..00000000
--- a/app/client/components/create-volume-form.tsx
+++ /dev/null
@@ -1,593 +0,0 @@
-import { arktypeResolver } from "@hookform/resolvers/arktype";
-import { useMutation } from "@tanstack/react-query";
-import { type } from "arktype";
-import { CheckCircle, Loader2, XCircle } from "lucide-react";
-import { useEffect, useState } from "react";
-import { useForm } from "react-hook-form";
-import { cn, slugify } from "~/client/lib/utils";
-import { deepClean } from "~/utils/object";
-import { DirectoryBrowser } from "./directory-browser";
-import { Button } from "./ui/button";
-import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "./ui/form";
-import { Input } from "./ui/input";
-import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
-import { volumeConfigSchema } from "~/schemas/volumes";
-import { testConnectionMutation } from "../api-client/@tanstack/react-query.gen";
-
-export const formSchema = type({
- name: "2<=string<=32",
-}).and(volumeConfigSchema);
-const cleanSchema = type.pipe((d) => formSchema(deepClean(d)));
-
-export type FormValues = typeof formSchema.inferIn;
-
-type Props = {
- onSubmit: (values: FormValues) => void;
- mode?: "create" | "update";
- initialValues?: Partial;
- formId?: string;
- loading?: boolean;
- className?: string;
-};
-
-const defaultValuesForType = {
- directory: { backend: "directory" as const, path: "/" },
- nfs: { backend: "nfs" as const, port: 2049, version: "4.1" as const },
- smb: { backend: "smb" as const, port: 445, vers: "3.0" as const },
- webdav: { backend: "webdav" as const, port: 80, ssl: false },
-};
-
-export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, formId, loading, className }: Props) => {
- const form = useForm({
- resolver: arktypeResolver(cleanSchema as unknown as typeof formSchema),
- defaultValues: initialValues,
- resetOptions: {
- keepDefaultValues: true,
- keepDirtyValues: false,
- },
- });
-
- const { watch, getValues } = form;
-
- const watchedBackend = watch("backend");
-
- useEffect(() => {
- if (mode === "create") {
- form.reset({
- name: form.getValues().name,
- ...defaultValuesForType[watchedBackend as keyof typeof defaultValuesForType],
- });
- }
- }, [watchedBackend, form, mode]);
-
- const [testMessage, setTestMessage] = useState<{ success: boolean; message: string } | null>(null);
-
- const testBackendConnection = useMutation({
- ...testConnectionMutation(),
- onMutate: () => {
- setTestMessage(null);
- },
- onError: (error) => {
- setTestMessage({
- success: false,
- message: error?.message || "Failed to test connection. Please try again.",
- });
- },
- onSuccess: (data) => {
- setTestMessage(data);
- },
- });
-
- const handleTestConnection = async () => {
- const formValues = getValues();
-
- if (formValues.backend === "nfs" || formValues.backend === "smb" || formValues.backend === "webdav") {
- testBackendConnection.mutate({
- body: { config: formValues },
- });
- }
- };
-
- return (
-
-
- );
-};
diff --git a/app/client/components/restore-form.tsx b/app/client/components/restore-form.tsx
index 4e4f7790..1891da87 100644
--- a/app/client/components/restore-form.tsx
+++ b/app/client/components/restore-form.tsx
@@ -164,6 +164,7 @@ export function RestoreForm({ snapshot, repositoryName, snapshotId, returnPath }
Cancel
- {formValues.includePatterns && formValues.includePatterns.length > 0 && (
+ {(formValues.includePatterns && formValues.includePatterns.length > 0) ||
+ formValues.includePatternsText ? (
-
Include paths
+
Include paths/patterns
- {formValues.includePatterns.map((path) => (
+ {formValues.includePatterns?.map((path) => (
{path}
))}
+ {formValues.includePatternsText
+ ?.split("\n")
+ .filter(Boolean)
+ .map((pattern) => (
+
+ {pattern.trim()}
+
+ ))}
- )}
+ ) : null}
{formValues.excludePatternsText && (
Exclude patterns
@@ -509,6 +614,21 @@ export const CreateScheduleForm = ({ initialValues, formId, onSubmit, volume }:
)}
+ {formValues.excludeIfPresentText && (
+
+
Exclude if present
+
+ {formValues.excludeIfPresentText
+ .split("\n")
+ .filter(Boolean)
+ .map((filename) => (
+
+ {filename.trim()}
+
+ ))}
+
+
+ )}
Retention
diff --git a/app/client/modules/backups/components/schedule-mirrors-config.tsx b/app/client/modules/backups/components/schedule-mirrors-config.tsx
new file mode 100644
index 00000000..419f5e0e
--- /dev/null
+++ b/app/client/modules/backups/components/schedule-mirrors-config.tsx
@@ -0,0 +1,352 @@
+import { useMutation, useQuery } from "@tanstack/react-query";
+import { Copy, Plus, Trash2 } from "lucide-react";
+import { useEffect, useMemo, useState } from "react";
+import { toast } from "sonner";
+import { Button } from "~/client/components/ui/button";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card";
+import { Switch } from "~/client/components/ui/switch";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/client/components/ui/select";
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/client/components/ui/table";
+import { Badge } from "~/client/components/ui/badge";
+import { Tooltip, TooltipContent, TooltipTrigger } from "~/client/components/ui/tooltip";
+import {
+ getScheduleMirrorsOptions,
+ getMirrorCompatibilityOptions,
+ updateScheduleMirrorsMutation,
+} from "~/client/api-client/@tanstack/react-query.gen";
+import { parseError } from "~/client/lib/errors";
+import type { Repository } from "~/client/lib/types";
+import { RepositoryIcon } from "~/client/components/repository-icon";
+import { StatusDot } from "~/client/components/status-dot";
+import { formatDistanceToNow } from "date-fns";
+import { Link } from "react-router";
+import { cn } from "~/client/lib/utils";
+
+type Props = {
+ scheduleId: number;
+ primaryRepositoryId: string;
+ repositories: Repository[];
+};
+
+type MirrorAssignment = {
+ repositoryId: string;
+ enabled: boolean;
+ lastCopyAt: number | null;
+ lastCopyStatus: "success" | "error" | null;
+ lastCopyError: string | null;
+};
+
+export const ScheduleMirrorsConfig = ({ scheduleId, primaryRepositoryId, repositories }: Props) => {
+ const [assignments, setAssignments] = useState
diff --git a/app/client/modules/volumes/components/volume-forms/directory-form.tsx b/app/client/modules/volumes/components/volume-forms/directory-form.tsx
new file mode 100644
index 00000000..4209c5f7
--- /dev/null
+++ b/app/client/modules/volumes/components/volume-forms/directory-form.tsx
@@ -0,0 +1,51 @@
+import { Pencil } from "lucide-react";
+import type { UseFormReturn } from "react-hook-form";
+import type { FormValues } from "../create-volume-form";
+import { DirectoryBrowser } from "../../../../components/directory-browser";
+import { Button } from "../../../../components/ui/button";
+import {
+ FormControl,
+ FormDescription,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "../../../../components/ui/form";
+
+type Props = {
+ form: UseFormReturn;
+};
+
+export const DirectoryForm = ({ form }: Props) => {
+ return (
+ {
+ return (
+
+ Directory Path
+
+ {field.value ? (
+
+
+
Selected path:
+
{field.value}
+
+
field.onChange("")}>
+
+ Change
+
+
+ ) : (
+ field.onChange(path)} selectedPath={field.value} />
+ )}
+
+ Browse and select a directory on the host filesystem to track.
+
+
+ );
+ }}
+ />
+ );
+};
diff --git a/app/client/modules/volumes/components/volume-forms/index.ts b/app/client/modules/volumes/components/volume-forms/index.ts
new file mode 100644
index 00000000..cdb2f2a3
--- /dev/null
+++ b/app/client/modules/volumes/components/volume-forms/index.ts
@@ -0,0 +1,5 @@
+export { DirectoryForm } from "./directory-form";
+export { NFSForm } from "./nfs-form";
+export { SMBForm } from "./smb-form";
+export { WebDAVForm } from "./webdav-form";
+export { RcloneForm } from "./rclone-form";
diff --git a/app/client/modules/volumes/components/volume-forms/nfs-form.tsx b/app/client/modules/volumes/components/volume-forms/nfs-form.tsx
new file mode 100644
index 00000000..e4ebb0a5
--- /dev/null
+++ b/app/client/modules/volumes/components/volume-forms/nfs-form.tsx
@@ -0,0 +1,120 @@
+import type { UseFormReturn } from "react-hook-form";
+import type { FormValues } from "../create-volume-form";
+import {
+ FormControl,
+ FormDescription,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "../../../../components/ui/form";
+import { Input } from "../../../../components/ui/input";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../../../../components/ui/select";
+
+type Props = {
+ form: UseFormReturn;
+};
+
+export const NFSForm = ({ form }: Props) => {
+ return (
+ <>
+ (
+
+ Server
+
+
+
+ NFS server IP address or hostname.
+
+
+ )}
+ />
+ (
+
+ Export Path
+
+
+
+ Path to the NFS export on the server.
+
+
+ )}
+ />
+ (
+
+ Port
+
+ field.onChange(parseInt(e.target.value, 10) || undefined)}
+ />
+
+ NFS server port (default: 2049).
+
+
+ )}
+ />
+ (
+
+ Version
+
+ NFS protocol version to use.
+
+
+ )}
+ />
+ (
+
+ Read-only Mode
+
+
+ field.onChange(e.target.checked)}
+ className="rounded border-gray-300"
+ />
+ Mount volume as read-only
+
+
+
+ Prevent any modifications to the volume. Recommended for backup sources and sensitive data.
+
+
+
+ )}
+ />
+ >
+ );
+};
diff --git a/app/client/modules/volumes/components/volume-forms/rclone-form.tsx b/app/client/modules/volumes/components/volume-forms/rclone-form.tsx
new file mode 100644
index 00000000..79ef10a8
--- /dev/null
+++ b/app/client/modules/volumes/components/volume-forms/rclone-form.tsx
@@ -0,0 +1,127 @@
+import { ExternalLink } from "lucide-react";
+import type { UseFormReturn } from "react-hook-form";
+import type { FormValues } from "../create-volume-form";
+import {
+ FormControl,
+ FormDescription,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "../../../../components/ui/form";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../../../../components/ui/select";
+import { Alert, AlertDescription } from "../../../../components/ui/alert";
+import { Input } from "../../../../components/ui/input";
+import { listRcloneRemotesOptions } from "~/client/api-client/@tanstack/react-query.gen";
+import { useSystemInfo } from "~/client/hooks/use-system-info";
+import { useQuery } from "@tanstack/react-query";
+
+type Props = {
+ form: UseFormReturn;
+};
+
+export const RcloneForm = ({ form }: Props) => {
+ const { capabilities } = useSystemInfo();
+
+ const { data: rcloneRemotes, isLoading: isLoadingRemotes } = useQuery({
+ ...listRcloneRemotesOptions(),
+ enabled: capabilities.rclone,
+ });
+
+ if (!isLoadingRemotes && (!rcloneRemotes || rcloneRemotes.length === 0)) {
+ return (
+
+
+ No rclone remotes configured
+
+ To use rclone, you need to configure remotes on your host system
+
+
+ View rclone documentation
+
+
+
+
+ );
+ }
+
+ return (
+ <>
+ (
+
+ Remote
+
+ Select the rclone remote configured on your host system.
+
+
+ )}
+ />
+ (
+
+ Path
+
+
+
+ Path on the remote to mount. Use "/" for the root.
+
+
+ )}
+ />
+ (
+
+ Read-only Mode
+
+
+ field.onChange(e.target.checked)}
+ className="rounded border-gray-300"
+ />
+ Mount volume as read-only
+
+
+
+ Prevent any modifications to the volume. Recommended for backup sources and sensitive data.
+
+
+
+ )}
+ />
+ >
+ );
+};
diff --git a/app/client/modules/volumes/components/volume-forms/smb-form.tsx b/app/client/modules/volumes/components/volume-forms/smb-form.tsx
new file mode 100644
index 00000000..7f995c46
--- /dev/null
+++ b/app/client/modules/volumes/components/volume-forms/smb-form.tsx
@@ -0,0 +1,163 @@
+import type { UseFormReturn } from "react-hook-form";
+import type { FormValues } from "../create-volume-form";
+import {
+ FormControl,
+ FormDescription,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "../../../../components/ui/form";
+import { Input } from "../../../../components/ui/input";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../../../../components/ui/select";
+
+type Props = {
+ form: UseFormReturn;
+};
+
+export const SMBForm = ({ form }: Props) => {
+ return (
+ <>
+ (
+
+ Server
+
+
+
+ SMB server IP address or hostname.
+
+
+ )}
+ />
+ (
+
+ Share
+
+
+
+ SMB share name on the server.
+
+
+ )}
+ />
+ (
+
+ Username
+
+
+
+ Username for SMB authentication.
+
+
+ )}
+ />
+ (
+
+ Password
+
+
+
+ Password for SMB authentication.
+
+
+ )}
+ />
+ (
+
+ SMB Version
+
+ SMB protocol version to use (default: 3.0).
+
+
+ )}
+ />
+ (
+
+ Domain (Optional)
+
+
+
+ Domain or workgroup for authentication (optional).
+
+
+ )}
+ />
+ (
+
+ Port
+
+ field.onChange(parseInt(e.target.value, 10) || undefined)}
+ />
+
+ SMB server port (default: 445).
+
+
+ )}
+ />
+ (
+
+ Read-only Mode
+
+
+ field.onChange(e.target.checked)}
+ className="rounded border-gray-300"
+ />
+ Mount volume as read-only
+
+
+
+ Prevent any modifications to the volume. Recommended for backup sources and sensitive data.
+
+
+
+ )}
+ />
+ >
+ );
+};
diff --git a/app/client/modules/volumes/components/volume-forms/webdav-form.tsx b/app/client/modules/volumes/components/volume-forms/webdav-form.tsx
new file mode 100644
index 00000000..1a399ed7
--- /dev/null
+++ b/app/client/modules/volumes/components/volume-forms/webdav-form.tsx
@@ -0,0 +1,146 @@
+import type { UseFormReturn } from "react-hook-form";
+import type { FormValues } from "../create-volume-form";
+import {
+ FormControl,
+ FormDescription,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "../../../../components/ui/form";
+import { Input } from "../../../../components/ui/input";
+
+type Props = {
+ form: UseFormReturn;
+};
+
+export const WebDAVForm = ({ form }: Props) => {
+ return (
+ <>
+ (
+
+ Server
+
+
+
+ WebDAV server hostname or IP address.
+
+
+ )}
+ />
+ (
+
+ Path
+
+
+
+ Path to the WebDAV directory on the server.
+
+
+ )}
+ />
+ (
+
+ Username (Optional)
+
+
+
+ Username for WebDAV authentication (optional).
+
+
+ )}
+ />
+ (
+
+ Password (Optional)
+
+
+
+ Password for WebDAV authentication (optional).
+
+
+ )}
+ />
+ (
+
+ Port
+
+ field.onChange(parseInt(e.target.value, 10) || undefined)}
+ />
+
+ WebDAV server port (default: 80 for HTTP, 443 for HTTPS).
+
+
+ )}
+ />
+ (
+
+ Use SSL/HTTPS
+
+
+ field.onChange(e.target.checked)}
+ className="rounded border-gray-300"
+ />
+ Enable HTTPS for secure connections
+
+
+ Use HTTPS instead of HTTP for secure connections.
+
+
+ )}
+ />
+ (
+
+ Read-only Mode
+
+
+ field.onChange(e.target.checked)}
+ className="rounded border-gray-300"
+ />
+ Mount volume as read-only
+
+
+
+ Prevent any modifications to the volume. Recommended for backup sources and sensitive data.
+
+
+
+ )}
+ />
+ >
+ );
+};
diff --git a/app/client/modules/volumes/routes/create-volume.tsx b/app/client/modules/volumes/routes/create-volume.tsx
index 15218ac8..e2faf6a6 100644
--- a/app/client/modules/volumes/routes/create-volume.tsx
+++ b/app/client/modules/volumes/routes/create-volume.tsx
@@ -1,10 +1,10 @@
import { useMutation } from "@tanstack/react-query";
-import { HardDrive } from "lucide-react";
+import { HardDrive, Plus } from "lucide-react";
import { useId } from "react";
import { useNavigate } from "react-router";
import { toast } from "sonner";
import { createVolumeMutation } from "~/client/api-client/@tanstack/react-query.gen";
-import { CreateVolumeForm, type FormValues } from "~/client/components/create-volume-form";
+import { CreateVolumeForm, type FormValues } from "~/client/modules/volumes/components/create-volume-form";
import { Button } from "~/client/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "~/client/components/ui/card";
import { parseError } from "~/client/lib/errors";
@@ -73,6 +73,7 @@ export default function CreateVolume() {
Cancel
+
Create Volume
diff --git a/app/client/modules/volumes/routes/volume-details.tsx b/app/client/modules/volumes/routes/volume-details.tsx
index 3ee690a3..082589c9 100644
--- a/app/client/modules/volumes/routes/volume-details.tsx
+++ b/app/client/modules/volumes/routes/volume-details.tsx
@@ -2,6 +2,7 @@ import { useMutation, useQuery } from "@tanstack/react-query";
import { useNavigate, useParams, useSearchParams } from "react-router";
import { toast } from "sonner";
import { useState } from "react";
+import { Plug, Unplug } from "lucide-react";
import { StatusDot } from "~/client/components/status-dot";
import { Button } from "~/client/components/ui/button";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/client/components/ui/tabs";
@@ -71,8 +72,6 @@ export default function VolumeDetails({ loaderData }: Route.ComponentProps) {
const { data } = useQuery({
...getVolumeOptions({ path: { name: name ?? "" } }),
initialData: loaderData,
- refetchInterval: 10000,
- refetchOnWindowFocus: true,
});
const { capabilities } = useSystemInfo();
@@ -150,6 +149,7 @@ export default function VolumeDetails({ loaderData }: Route.ComponentProps) {
loading={mountVol.isPending}
className={cn({ hidden: volume.status === "mounted" })}
>
+
Mount
+
Unmount
setShowDeleteConfirm(true)} disabled={deleteVol.isPending}>
diff --git a/app/client/modules/volumes/routes/volumes.tsx b/app/client/modules/volumes/routes/volumes.tsx
index fc8bbda4..3a2bc14b 100644
--- a/app/client/modules/volumes/routes/volumes.tsx
+++ b/app/client/modules/volumes/routes/volumes.tsx
@@ -61,8 +61,6 @@ export default function Volumes({ loaderData }: Route.ComponentProps) {
const { data } = useQuery({
...listVolumesOptions(),
initialData: loaderData,
- refetchInterval: 10000,
- refetchOnWindowFocus: true,
});
const filteredVolumes =
diff --git a/app/client/modules/volumes/tabs/info.tsx b/app/client/modules/volumes/tabs/info.tsx
index 2b21307d..79a9c200 100644
--- a/app/client/modules/volumes/tabs/info.tsx
+++ b/app/client/modules/volumes/tabs/info.tsx
@@ -2,7 +2,8 @@ import { useMutation } from "@tanstack/react-query";
import { useState } from "react";
import { useNavigate } from "react-router";
import { toast } from "sonner";
-import { CreateVolumeForm, type FormValues } from "~/client/components/create-volume-form";
+import { Check } from "lucide-react";
+import { CreateVolumeForm, type FormValues } from "~/client/modules/volumes/components/create-volume-form";
import {
AlertDialog,
AlertDialogAction,
@@ -94,7 +95,10 @@ export const VolumeInfoTabContent = ({ volume, statfs }: Props) => {
Cancel
- Update
+
+
+ Update
+
diff --git a/app/drizzle/0018_breezy_invaders.sql b/app/drizzle/0018_breezy_invaders.sql
new file mode 100644
index 00000000..7a3e100b
--- /dev/null
+++ b/app/drizzle/0018_breezy_invaders.sql
@@ -0,0 +1,139 @@
+CREATE TABLE `backup_schedule_mirrors_table` (
+ `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
+ `schedule_id` integer NOT NULL,
+ `repository_id` text NOT NULL,
+ `enabled` integer DEFAULT true NOT NULL,
+ `last_copy_at` integer,
+ `last_copy_status` text,
+ `last_copy_error` text,
+ `created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL,
+ FOREIGN KEY (`schedule_id`) REFERENCES `backup_schedules_table`(`id`) ON UPDATE no action ON DELETE cascade,
+ FOREIGN KEY (`repository_id`) REFERENCES `repositories_table`(`id`) ON UPDATE no action ON DELETE cascade
+);
+--> statement-breakpoint
+CREATE UNIQUE INDEX `backup_schedule_mirrors_table_schedule_id_repository_id_unique` ON `backup_schedule_mirrors_table` (`schedule_id`,`repository_id`);--> statement-breakpoint
+PRAGMA foreign_keys=OFF;--> statement-breakpoint
+CREATE TABLE `__new_app_metadata` (
+ `key` text PRIMARY KEY NOT NULL,
+ `value` text NOT NULL,
+ `created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL,
+ `updated_at` integer DEFAULT (unixepoch() * 1000) NOT NULL
+);
+--> statement-breakpoint
+INSERT INTO `__new_app_metadata`("key", "value", "created_at", "updated_at") SELECT "key", "value", "created_at", "updated_at" FROM `app_metadata`;--> statement-breakpoint
+DROP TABLE `app_metadata`;--> statement-breakpoint
+ALTER TABLE `__new_app_metadata` RENAME TO `app_metadata`;--> statement-breakpoint
+PRAGMA foreign_keys=ON;--> statement-breakpoint
+CREATE TABLE `__new_backup_schedule_notifications_table` (
+ `schedule_id` integer NOT NULL,
+ `destination_id` integer NOT NULL,
+ `notify_on_start` integer DEFAULT false NOT NULL,
+ `notify_on_success` integer DEFAULT false NOT NULL,
+ `notify_on_failure` integer DEFAULT true NOT NULL,
+ `created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL,
+ PRIMARY KEY(`schedule_id`, `destination_id`),
+ FOREIGN KEY (`schedule_id`) REFERENCES `backup_schedules_table`(`id`) ON UPDATE no action ON DELETE cascade,
+ FOREIGN KEY (`destination_id`) REFERENCES `notification_destinations_table`(`id`) ON UPDATE no action ON DELETE cascade
+);
+--> statement-breakpoint
+INSERT INTO `__new_backup_schedule_notifications_table`("schedule_id", "destination_id", "notify_on_start", "notify_on_success", "notify_on_failure", "created_at") SELECT "schedule_id", "destination_id", "notify_on_start", "notify_on_success", "notify_on_failure", "created_at" FROM `backup_schedule_notifications_table`;--> statement-breakpoint
+DROP TABLE `backup_schedule_notifications_table`;--> statement-breakpoint
+ALTER TABLE `__new_backup_schedule_notifications_table` RENAME TO `backup_schedule_notifications_table`;--> statement-breakpoint
+CREATE TABLE `__new_backup_schedules_table` (
+ `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
+ `volume_id` integer NOT NULL,
+ `repository_id` text NOT NULL,
+ `enabled` integer DEFAULT true NOT NULL,
+ `cron_expression` text NOT NULL,
+ `retention_policy` text,
+ `exclude_patterns` text DEFAULT '[]',
+ `include_patterns` text DEFAULT '[]',
+ `last_backup_at` integer,
+ `last_backup_status` text,
+ `last_backup_error` text,
+ `next_backup_at` integer,
+ `created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL,
+ `updated_at` integer DEFAULT (unixepoch() * 1000) NOT NULL,
+ FOREIGN KEY (`volume_id`) REFERENCES `volumes_table`(`id`) ON UPDATE no action ON DELETE cascade,
+ FOREIGN KEY (`repository_id`) REFERENCES `repositories_table`(`id`) ON UPDATE no action ON DELETE cascade
+);
+--> statement-breakpoint
+INSERT INTO `__new_backup_schedules_table`("id", "volume_id", "repository_id", "enabled", "cron_expression", "retention_policy", "exclude_patterns", "include_patterns", "last_backup_at", "last_backup_status", "last_backup_error", "next_backup_at", "created_at", "updated_at") SELECT "id", "volume_id", "repository_id", "enabled", "cron_expression", "retention_policy", "exclude_patterns", "include_patterns", "last_backup_at", "last_backup_status", "last_backup_error", "next_backup_at", "created_at", "updated_at" FROM `backup_schedules_table`;--> statement-breakpoint
+DROP TABLE `backup_schedules_table`;--> statement-breakpoint
+ALTER TABLE `__new_backup_schedules_table` RENAME TO `backup_schedules_table`;--> statement-breakpoint
+CREATE TABLE `__new_notification_destinations_table` (
+ `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
+ `name` text NOT NULL,
+ `enabled` integer DEFAULT true NOT NULL,
+ `type` text NOT NULL,
+ `config` text NOT NULL,
+ `created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL,
+ `updated_at` integer DEFAULT (unixepoch() * 1000) NOT NULL
+);
+--> statement-breakpoint
+INSERT INTO `__new_notification_destinations_table`("id", "name", "enabled", "type", "config", "created_at", "updated_at") SELECT "id", "name", "enabled", "type", "config", "created_at", "updated_at" FROM `notification_destinations_table`;--> statement-breakpoint
+DROP TABLE `notification_destinations_table`;--> statement-breakpoint
+ALTER TABLE `__new_notification_destinations_table` RENAME TO `notification_destinations_table`;--> statement-breakpoint
+CREATE UNIQUE INDEX `notification_destinations_table_name_unique` ON `notification_destinations_table` (`name`);--> statement-breakpoint
+CREATE TABLE `__new_repositories_table` (
+ `id` text PRIMARY KEY NOT NULL,
+ `short_id` text NOT NULL,
+ `name` text NOT NULL,
+ `type` text NOT NULL,
+ `config` text NOT NULL,
+ `compression_mode` text DEFAULT 'auto',
+ `status` text DEFAULT 'unknown',
+ `last_checked` integer,
+ `last_error` text,
+ `created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL,
+ `updated_at` integer DEFAULT (unixepoch() * 1000) NOT NULL
+);
+--> statement-breakpoint
+INSERT INTO `__new_repositories_table`("id", "short_id", "name", "type", "config", "compression_mode", "status", "last_checked", "last_error", "created_at", "updated_at") SELECT "id", "short_id", "name", "type", "config", "compression_mode", "status", "last_checked", "last_error", "created_at", "updated_at" FROM `repositories_table`;--> statement-breakpoint
+DROP TABLE `repositories_table`;--> statement-breakpoint
+ALTER TABLE `__new_repositories_table` RENAME TO `repositories_table`;--> statement-breakpoint
+CREATE UNIQUE INDEX `repositories_table_short_id_unique` ON `repositories_table` (`short_id`);--> statement-breakpoint
+CREATE UNIQUE INDEX `repositories_table_name_unique` ON `repositories_table` (`name`);--> statement-breakpoint
+CREATE TABLE `__new_sessions_table` (
+ `id` text PRIMARY KEY NOT NULL,
+ `user_id` integer NOT NULL,
+ `expires_at` integer NOT NULL,
+ `created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL,
+ FOREIGN KEY (`user_id`) REFERENCES `users_table`(`id`) ON UPDATE no action ON DELETE cascade
+);
+--> statement-breakpoint
+INSERT INTO `__new_sessions_table`("id", "user_id", "expires_at", "created_at") SELECT "id", "user_id", "expires_at", "created_at" FROM `sessions_table`;--> statement-breakpoint
+DROP TABLE `sessions_table`;--> statement-breakpoint
+ALTER TABLE `__new_sessions_table` RENAME TO `sessions_table`;--> statement-breakpoint
+CREATE TABLE `__new_users_table` (
+ `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
+ `username` text NOT NULL,
+ `password_hash` text NOT NULL,
+ `has_downloaded_restic_password` integer DEFAULT false NOT NULL,
+ `created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL,
+ `updated_at` integer DEFAULT (unixepoch() * 1000) NOT NULL
+);
+--> statement-breakpoint
+INSERT INTO `__new_users_table`("id", "username", "password_hash", "has_downloaded_restic_password", "created_at", "updated_at") SELECT "id", "username", "password_hash", "has_downloaded_restic_password", "created_at", "updated_at" FROM `users_table`;--> statement-breakpoint
+DROP TABLE `users_table`;--> statement-breakpoint
+ALTER TABLE `__new_users_table` RENAME TO `users_table`;--> statement-breakpoint
+CREATE UNIQUE INDEX `users_table_username_unique` ON `users_table` (`username`);--> statement-breakpoint
+CREATE TABLE `__new_volumes_table` (
+ `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
+ `short_id` text NOT NULL,
+ `name` text NOT NULL,
+ `type` text NOT NULL,
+ `status` text DEFAULT 'unmounted' NOT NULL,
+ `last_error` text,
+ `last_health_check` integer DEFAULT (unixepoch() * 1000) NOT NULL,
+ `created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL,
+ `updated_at` integer DEFAULT (unixepoch() * 1000) NOT NULL,
+ `config` text NOT NULL,
+ `auto_remount` integer DEFAULT true NOT NULL
+);
+--> statement-breakpoint
+INSERT INTO `__new_volumes_table`("id", "short_id", "name", "type", "status", "last_error", "last_health_check", "created_at", "updated_at", "config", "auto_remount") SELECT "id", "short_id", "name", "type", "status", "last_error", "last_health_check", "created_at", "updated_at", "config", "auto_remount" FROM `volumes_table`;--> statement-breakpoint
+DROP TABLE `volumes_table`;--> statement-breakpoint
+ALTER TABLE `__new_volumes_table` RENAME TO `volumes_table`;--> statement-breakpoint
+CREATE UNIQUE INDEX `volumes_table_short_id_unique` ON `volumes_table` (`short_id`);--> statement-breakpoint
+CREATE UNIQUE INDEX `volumes_table_name_unique` ON `volumes_table` (`name`);
\ No newline at end of file
diff --git a/app/drizzle/0019_secret_nomad.sql b/app/drizzle/0019_secret_nomad.sql
new file mode 100644
index 00000000..b8e3bbbd
--- /dev/null
+++ b/app/drizzle/0019_secret_nomad.sql
@@ -0,0 +1,24 @@
+PRAGMA foreign_keys=OFF;--> statement-breakpoint
+CREATE TABLE `__new_backup_schedules_table` (
+ `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
+ `name` text NOT NULL,
+ `volume_id` integer NOT NULL REFERENCES `volumes_table`(`id`) ON DELETE CASCADE,
+ `repository_id` text NOT NULL REFERENCES `repositories_table`(`id`) ON DELETE CASCADE,
+ `enabled` integer DEFAULT true NOT NULL,
+ `cron_expression` text NOT NULL,
+ `retention_policy` text,
+ `exclude_patterns` text DEFAULT '[]',
+ `include_patterns` text DEFAULT '[]',
+ `last_backup_at` integer,
+ `last_backup_status` text,
+ `last_backup_error` text,
+ `next_backup_at` integer,
+ `created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL,
+ `updated_at` integer DEFAULT (unixepoch() * 1000) NOT NULL
+);--> statement-breakpoint
+INSERT INTO `__new_backup_schedules_table`(`id`, `name`, `volume_id`, `repository_id`, `enabled`, `cron_expression`, `retention_policy`, `exclude_patterns`, `include_patterns`, `last_backup_at`, `last_backup_status`, `last_backup_error`, `next_backup_at`, `created_at`, `updated_at`)
+SELECT `id`, lower(hex(randomblob(3))), `volume_id`, `repository_id`, `enabled`, `cron_expression`, `retention_policy`, `exclude_patterns`, `include_patterns`, `last_backup_at`, `last_backup_status`, `last_backup_error`, `next_backup_at`, `created_at`, `updated_at` FROM `backup_schedules_table`;--> statement-breakpoint
+DROP TABLE `backup_schedules_table`;--> statement-breakpoint
+ALTER TABLE `__new_backup_schedules_table` RENAME TO `backup_schedules_table`;--> statement-breakpoint
+PRAGMA foreign_keys=ON;--> statement-breakpoint
+CREATE UNIQUE INDEX `backup_schedules_table_name_unique` ON `backup_schedules_table` (`name`);
\ No newline at end of file
diff --git a/app/drizzle/0020_even_dexter_bennett.sql b/app/drizzle/0020_even_dexter_bennett.sql
new file mode 100644
index 00000000..82196245
--- /dev/null
+++ b/app/drizzle/0020_even_dexter_bennett.sql
@@ -0,0 +1 @@
+ALTER TABLE `backup_schedules_table` ADD `exclude_if_present` text DEFAULT '[]';
\ No newline at end of file
diff --git a/app/drizzle/0021_steady_viper.sql b/app/drizzle/0021_steady_viper.sql
new file mode 100644
index 00000000..0f3b456c
--- /dev/null
+++ b/app/drizzle/0021_steady_viper.sql
@@ -0,0 +1 @@
+ALTER TABLE `backup_schedule_notifications_table` ADD `notify_on_warning` integer DEFAULT true NOT NULL;
\ No newline at end of file
diff --git a/app/drizzle/meta/0018_snapshot.json b/app/drizzle/meta/0018_snapshot.json
new file mode 100644
index 00000000..27420d26
--- /dev/null
+++ b/app/drizzle/meta/0018_snapshot.json
@@ -0,0 +1,792 @@
+{
+ "version": "6",
+ "dialect": "sqlite",
+ "id": "d5a60aea-4490-423e-8725-6ace87a76c9b",
+ "prevId": "d0bfd316-b8f5-459b-ab17-0ce679479321",
+ "tables": {
+ "app_metadata": {
+ "name": "app_metadata",
+ "columns": {
+ "key": {
+ "name": "key",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "value": {
+ "name": "value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "backup_schedule_mirrors_table": {
+ "name": "backup_schedule_mirrors_table",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "schedule_id": {
+ "name": "schedule_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "repository_id": {
+ "name": "repository_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": true
+ },
+ "last_copy_at": {
+ "name": "last_copy_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "last_copy_status": {
+ "name": "last_copy_status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "last_copy_error": {
+ "name": "last_copy_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)"
+ }
+ },
+ "indexes": {
+ "backup_schedule_mirrors_table_schedule_id_repository_id_unique": {
+ "name": "backup_schedule_mirrors_table_schedule_id_repository_id_unique",
+ "columns": [
+ "schedule_id",
+ "repository_id"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {
+ "backup_schedule_mirrors_table_schedule_id_backup_schedules_table_id_fk": {
+ "name": "backup_schedule_mirrors_table_schedule_id_backup_schedules_table_id_fk",
+ "tableFrom": "backup_schedule_mirrors_table",
+ "tableTo": "backup_schedules_table",
+ "columnsFrom": [
+ "schedule_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "backup_schedule_mirrors_table_repository_id_repositories_table_id_fk": {
+ "name": "backup_schedule_mirrors_table_repository_id_repositories_table_id_fk",
+ "tableFrom": "backup_schedule_mirrors_table",
+ "tableTo": "repositories_table",
+ "columnsFrom": [
+ "repository_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "backup_schedule_notifications_table": {
+ "name": "backup_schedule_notifications_table",
+ "columns": {
+ "schedule_id": {
+ "name": "schedule_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "destination_id": {
+ "name": "destination_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "notify_on_start": {
+ "name": "notify_on_start",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "notify_on_success": {
+ "name": "notify_on_success",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "notify_on_failure": {
+ "name": "notify_on_failure",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "backup_schedule_notifications_table_schedule_id_backup_schedules_table_id_fk": {
+ "name": "backup_schedule_notifications_table_schedule_id_backup_schedules_table_id_fk",
+ "tableFrom": "backup_schedule_notifications_table",
+ "tableTo": "backup_schedules_table",
+ "columnsFrom": [
+ "schedule_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "backup_schedule_notifications_table_destination_id_notification_destinations_table_id_fk": {
+ "name": "backup_schedule_notifications_table_destination_id_notification_destinations_table_id_fk",
+ "tableFrom": "backup_schedule_notifications_table",
+ "tableTo": "notification_destinations_table",
+ "columnsFrom": [
+ "destination_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "backup_schedule_notifications_table_schedule_id_destination_id_pk": {
+ "columns": [
+ "schedule_id",
+ "destination_id"
+ ],
+ "name": "backup_schedule_notifications_table_schedule_id_destination_id_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "backup_schedules_table": {
+ "name": "backup_schedules_table",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "volume_id": {
+ "name": "volume_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "repository_id": {
+ "name": "repository_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": true
+ },
+ "cron_expression": {
+ "name": "cron_expression",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "retention_policy": {
+ "name": "retention_policy",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "exclude_patterns": {
+ "name": "exclude_patterns",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'[]'"
+ },
+ "include_patterns": {
+ "name": "include_patterns",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'[]'"
+ },
+ "last_backup_at": {
+ "name": "last_backup_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "last_backup_status": {
+ "name": "last_backup_status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "last_backup_error": {
+ "name": "last_backup_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "next_backup_at": {
+ "name": "next_backup_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "backup_schedules_table_volume_id_volumes_table_id_fk": {
+ "name": "backup_schedules_table_volume_id_volumes_table_id_fk",
+ "tableFrom": "backup_schedules_table",
+ "tableTo": "volumes_table",
+ "columnsFrom": [
+ "volume_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "backup_schedules_table_repository_id_repositories_table_id_fk": {
+ "name": "backup_schedules_table_repository_id_repositories_table_id_fk",
+ "tableFrom": "backup_schedules_table",
+ "tableTo": "repositories_table",
+ "columnsFrom": [
+ "repository_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "notification_destinations_table": {
+ "name": "notification_destinations_table",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": true
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "config": {
+ "name": "config",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)"
+ }
+ },
+ "indexes": {
+ "notification_destinations_table_name_unique": {
+ "name": "notification_destinations_table_name_unique",
+ "columns": [
+ "name"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "repositories_table": {
+ "name": "repositories_table",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "short_id": {
+ "name": "short_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "config": {
+ "name": "config",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "compression_mode": {
+ "name": "compression_mode",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'auto'"
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'unknown'"
+ },
+ "last_checked": {
+ "name": "last_checked",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "last_error": {
+ "name": "last_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)"
+ }
+ },
+ "indexes": {
+ "repositories_table_short_id_unique": {
+ "name": "repositories_table_short_id_unique",
+ "columns": [
+ "short_id"
+ ],
+ "isUnique": true
+ },
+ "repositories_table_name_unique": {
+ "name": "repositories_table_name_unique",
+ "columns": [
+ "name"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "sessions_table": {
+ "name": "sessions_table",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "sessions_table_user_id_users_table_id_fk": {
+ "name": "sessions_table_user_id_users_table_id_fk",
+ "tableFrom": "sessions_table",
+ "tableTo": "users_table",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "users_table": {
+ "name": "users_table",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "username": {
+ "name": "username",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "password_hash": {
+ "name": "password_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "has_downloaded_restic_password": {
+ "name": "has_downloaded_restic_password",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)"
+ }
+ },
+ "indexes": {
+ "users_table_username_unique": {
+ "name": "users_table_username_unique",
+ "columns": [
+ "username"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "volumes_table": {
+ "name": "volumes_table",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "short_id": {
+ "name": "short_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'unmounted'"
+ },
+ "last_error": {
+ "name": "last_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "last_health_check": {
+ "name": "last_health_check",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)"
+ },
+ "config": {
+ "name": "config",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "auto_remount": {
+ "name": "auto_remount",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": true
+ }
+ },
+ "indexes": {
+ "volumes_table_short_id_unique": {
+ "name": "volumes_table_short_id_unique",
+ "columns": [
+ "short_id"
+ ],
+ "isUnique": true
+ },
+ "volumes_table_name_unique": {
+ "name": "volumes_table_name_unique",
+ "columns": [
+ "name"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ }
+ },
+ "views": {},
+ "enums": {},
+ "_meta": {
+ "schemas": {},
+ "tables": {},
+ "columns": {}
+ },
+ "internal": {
+ "indexes": {}
+ }
+}
\ No newline at end of file
diff --git a/app/drizzle/meta/0019_snapshot.json b/app/drizzle/meta/0019_snapshot.json
new file mode 100644
index 00000000..3f2d178c
--- /dev/null
+++ b/app/drizzle/meta/0019_snapshot.json
@@ -0,0 +1,807 @@
+{
+ "version": "6",
+ "dialect": "sqlite",
+ "id": "b5b3acff-51d7-45ae-b9d2-4b07a6286fc3",
+ "prevId": "d5a60aea-4490-423e-8725-6ace87a76c9b",
+ "tables": {
+ "app_metadata": {
+ "name": "app_metadata",
+ "columns": {
+ "key": {
+ "name": "key",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "value": {
+ "name": "value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "backup_schedule_mirrors_table": {
+ "name": "backup_schedule_mirrors_table",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "schedule_id": {
+ "name": "schedule_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "repository_id": {
+ "name": "repository_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": true
+ },
+ "last_copy_at": {
+ "name": "last_copy_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "last_copy_status": {
+ "name": "last_copy_status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "last_copy_error": {
+ "name": "last_copy_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)"
+ }
+ },
+ "indexes": {
+ "backup_schedule_mirrors_table_schedule_id_repository_id_unique": {
+ "name": "backup_schedule_mirrors_table_schedule_id_repository_id_unique",
+ "columns": [
+ "schedule_id",
+ "repository_id"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {
+ "backup_schedule_mirrors_table_schedule_id_backup_schedules_table_id_fk": {
+ "name": "backup_schedule_mirrors_table_schedule_id_backup_schedules_table_id_fk",
+ "tableFrom": "backup_schedule_mirrors_table",
+ "tableTo": "backup_schedules_table",
+ "columnsFrom": [
+ "schedule_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "backup_schedule_mirrors_table_repository_id_repositories_table_id_fk": {
+ "name": "backup_schedule_mirrors_table_repository_id_repositories_table_id_fk",
+ "tableFrom": "backup_schedule_mirrors_table",
+ "tableTo": "repositories_table",
+ "columnsFrom": [
+ "repository_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "backup_schedule_notifications_table": {
+ "name": "backup_schedule_notifications_table",
+ "columns": {
+ "schedule_id": {
+ "name": "schedule_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "destination_id": {
+ "name": "destination_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "notify_on_start": {
+ "name": "notify_on_start",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "notify_on_success": {
+ "name": "notify_on_success",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "notify_on_failure": {
+ "name": "notify_on_failure",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "backup_schedule_notifications_table_schedule_id_backup_schedules_table_id_fk": {
+ "name": "backup_schedule_notifications_table_schedule_id_backup_schedules_table_id_fk",
+ "tableFrom": "backup_schedule_notifications_table",
+ "tableTo": "backup_schedules_table",
+ "columnsFrom": [
+ "schedule_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "backup_schedule_notifications_table_destination_id_notification_destinations_table_id_fk": {
+ "name": "backup_schedule_notifications_table_destination_id_notification_destinations_table_id_fk",
+ "tableFrom": "backup_schedule_notifications_table",
+ "tableTo": "notification_destinations_table",
+ "columnsFrom": [
+ "destination_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "backup_schedule_notifications_table_schedule_id_destination_id_pk": {
+ "columns": [
+ "schedule_id",
+ "destination_id"
+ ],
+ "name": "backup_schedule_notifications_table_schedule_id_destination_id_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "backup_schedules_table": {
+ "name": "backup_schedules_table",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "volume_id": {
+ "name": "volume_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "repository_id": {
+ "name": "repository_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": true
+ },
+ "cron_expression": {
+ "name": "cron_expression",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "retention_policy": {
+ "name": "retention_policy",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "exclude_patterns": {
+ "name": "exclude_patterns",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'[]'"
+ },
+ "include_patterns": {
+ "name": "include_patterns",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'[]'"
+ },
+ "last_backup_at": {
+ "name": "last_backup_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "last_backup_status": {
+ "name": "last_backup_status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "last_backup_error": {
+ "name": "last_backup_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "next_backup_at": {
+ "name": "next_backup_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)"
+ }
+ },
+ "indexes": {
+ "backup_schedules_table_name_unique": {
+ "name": "backup_schedules_table_name_unique",
+ "columns": [
+ "name"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {
+ "backup_schedules_table_volume_id_volumes_table_id_fk": {
+ "name": "backup_schedules_table_volume_id_volumes_table_id_fk",
+ "tableFrom": "backup_schedules_table",
+ "tableTo": "volumes_table",
+ "columnsFrom": [
+ "volume_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "backup_schedules_table_repository_id_repositories_table_id_fk": {
+ "name": "backup_schedules_table_repository_id_repositories_table_id_fk",
+ "tableFrom": "backup_schedules_table",
+ "tableTo": "repositories_table",
+ "columnsFrom": [
+ "repository_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "notification_destinations_table": {
+ "name": "notification_destinations_table",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": true
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "config": {
+ "name": "config",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)"
+ }
+ },
+ "indexes": {
+ "notification_destinations_table_name_unique": {
+ "name": "notification_destinations_table_name_unique",
+ "columns": [
+ "name"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "repositories_table": {
+ "name": "repositories_table",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "short_id": {
+ "name": "short_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "config": {
+ "name": "config",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "compression_mode": {
+ "name": "compression_mode",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'auto'"
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'unknown'"
+ },
+ "last_checked": {
+ "name": "last_checked",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "last_error": {
+ "name": "last_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)"
+ }
+ },
+ "indexes": {
+ "repositories_table_short_id_unique": {
+ "name": "repositories_table_short_id_unique",
+ "columns": [
+ "short_id"
+ ],
+ "isUnique": true
+ },
+ "repositories_table_name_unique": {
+ "name": "repositories_table_name_unique",
+ "columns": [
+ "name"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "sessions_table": {
+ "name": "sessions_table",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "sessions_table_user_id_users_table_id_fk": {
+ "name": "sessions_table_user_id_users_table_id_fk",
+ "tableFrom": "sessions_table",
+ "tableTo": "users_table",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "users_table": {
+ "name": "users_table",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "username": {
+ "name": "username",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "password_hash": {
+ "name": "password_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "has_downloaded_restic_password": {
+ "name": "has_downloaded_restic_password",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)"
+ }
+ },
+ "indexes": {
+ "users_table_username_unique": {
+ "name": "users_table_username_unique",
+ "columns": [
+ "username"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "volumes_table": {
+ "name": "volumes_table",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "short_id": {
+ "name": "short_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'unmounted'"
+ },
+ "last_error": {
+ "name": "last_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "last_health_check": {
+ "name": "last_health_check",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)"
+ },
+ "config": {
+ "name": "config",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "auto_remount": {
+ "name": "auto_remount",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": true
+ }
+ },
+ "indexes": {
+ "volumes_table_short_id_unique": {
+ "name": "volumes_table_short_id_unique",
+ "columns": [
+ "short_id"
+ ],
+ "isUnique": true
+ },
+ "volumes_table_name_unique": {
+ "name": "volumes_table_name_unique",
+ "columns": [
+ "name"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ }
+ },
+ "views": {},
+ "enums": {},
+ "_meta": {
+ "schemas": {},
+ "tables": {},
+ "columns": {}
+ },
+ "internal": {
+ "indexes": {}
+ }
+}
\ No newline at end of file
diff --git a/app/drizzle/meta/0020_snapshot.json b/app/drizzle/meta/0020_snapshot.json
new file mode 100644
index 00000000..3c30048b
--- /dev/null
+++ b/app/drizzle/meta/0020_snapshot.json
@@ -0,0 +1,815 @@
+{
+ "version": "6",
+ "dialect": "sqlite",
+ "id": "729d3ce9-b4b9-41f6-a270-d74c96510238",
+ "prevId": "b5b3acff-51d7-45ae-b9d2-4b07a6286fc3",
+ "tables": {
+ "app_metadata": {
+ "name": "app_metadata",
+ "columns": {
+ "key": {
+ "name": "key",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "value": {
+ "name": "value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "backup_schedule_mirrors_table": {
+ "name": "backup_schedule_mirrors_table",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "schedule_id": {
+ "name": "schedule_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "repository_id": {
+ "name": "repository_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": true
+ },
+ "last_copy_at": {
+ "name": "last_copy_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "last_copy_status": {
+ "name": "last_copy_status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "last_copy_error": {
+ "name": "last_copy_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)"
+ }
+ },
+ "indexes": {
+ "backup_schedule_mirrors_table_schedule_id_repository_id_unique": {
+ "name": "backup_schedule_mirrors_table_schedule_id_repository_id_unique",
+ "columns": [
+ "schedule_id",
+ "repository_id"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {
+ "backup_schedule_mirrors_table_schedule_id_backup_schedules_table_id_fk": {
+ "name": "backup_schedule_mirrors_table_schedule_id_backup_schedules_table_id_fk",
+ "tableFrom": "backup_schedule_mirrors_table",
+ "tableTo": "backup_schedules_table",
+ "columnsFrom": [
+ "schedule_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "backup_schedule_mirrors_table_repository_id_repositories_table_id_fk": {
+ "name": "backup_schedule_mirrors_table_repository_id_repositories_table_id_fk",
+ "tableFrom": "backup_schedule_mirrors_table",
+ "tableTo": "repositories_table",
+ "columnsFrom": [
+ "repository_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "backup_schedule_notifications_table": {
+ "name": "backup_schedule_notifications_table",
+ "columns": {
+ "schedule_id": {
+ "name": "schedule_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "destination_id": {
+ "name": "destination_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "notify_on_start": {
+ "name": "notify_on_start",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "notify_on_success": {
+ "name": "notify_on_success",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "notify_on_failure": {
+ "name": "notify_on_failure",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "backup_schedule_notifications_table_schedule_id_backup_schedules_table_id_fk": {
+ "name": "backup_schedule_notifications_table_schedule_id_backup_schedules_table_id_fk",
+ "tableFrom": "backup_schedule_notifications_table",
+ "tableTo": "backup_schedules_table",
+ "columnsFrom": [
+ "schedule_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "backup_schedule_notifications_table_destination_id_notification_destinations_table_id_fk": {
+ "name": "backup_schedule_notifications_table_destination_id_notification_destinations_table_id_fk",
+ "tableFrom": "backup_schedule_notifications_table",
+ "tableTo": "notification_destinations_table",
+ "columnsFrom": [
+ "destination_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "backup_schedule_notifications_table_schedule_id_destination_id_pk": {
+ "columns": [
+ "schedule_id",
+ "destination_id"
+ ],
+ "name": "backup_schedule_notifications_table_schedule_id_destination_id_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "backup_schedules_table": {
+ "name": "backup_schedules_table",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "volume_id": {
+ "name": "volume_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "repository_id": {
+ "name": "repository_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": true
+ },
+ "cron_expression": {
+ "name": "cron_expression",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "retention_policy": {
+ "name": "retention_policy",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "exclude_patterns": {
+ "name": "exclude_patterns",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'[]'"
+ },
+ "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
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)"
+ }
+ },
+ "indexes": {
+ "backup_schedules_table_name_unique": {
+ "name": "backup_schedules_table_name_unique",
+ "columns": [
+ "name"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {
+ "backup_schedules_table_volume_id_volumes_table_id_fk": {
+ "name": "backup_schedules_table_volume_id_volumes_table_id_fk",
+ "tableFrom": "backup_schedules_table",
+ "tableTo": "volumes_table",
+ "columnsFrom": [
+ "volume_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "backup_schedules_table_repository_id_repositories_table_id_fk": {
+ "name": "backup_schedules_table_repository_id_repositories_table_id_fk",
+ "tableFrom": "backup_schedules_table",
+ "tableTo": "repositories_table",
+ "columnsFrom": [
+ "repository_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "notification_destinations_table": {
+ "name": "notification_destinations_table",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": true
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "config": {
+ "name": "config",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)"
+ }
+ },
+ "indexes": {
+ "notification_destinations_table_name_unique": {
+ "name": "notification_destinations_table_name_unique",
+ "columns": [
+ "name"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "repositories_table": {
+ "name": "repositories_table",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "short_id": {
+ "name": "short_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "config": {
+ "name": "config",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "compression_mode": {
+ "name": "compression_mode",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'auto'"
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'unknown'"
+ },
+ "last_checked": {
+ "name": "last_checked",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "last_error": {
+ "name": "last_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)"
+ }
+ },
+ "indexes": {
+ "repositories_table_short_id_unique": {
+ "name": "repositories_table_short_id_unique",
+ "columns": [
+ "short_id"
+ ],
+ "isUnique": true
+ },
+ "repositories_table_name_unique": {
+ "name": "repositories_table_name_unique",
+ "columns": [
+ "name"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "sessions_table": {
+ "name": "sessions_table",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "sessions_table_user_id_users_table_id_fk": {
+ "name": "sessions_table_user_id_users_table_id_fk",
+ "tableFrom": "sessions_table",
+ "tableTo": "users_table",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "users_table": {
+ "name": "users_table",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "username": {
+ "name": "username",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "password_hash": {
+ "name": "password_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "has_downloaded_restic_password": {
+ "name": "has_downloaded_restic_password",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)"
+ }
+ },
+ "indexes": {
+ "users_table_username_unique": {
+ "name": "users_table_username_unique",
+ "columns": [
+ "username"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "volumes_table": {
+ "name": "volumes_table",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "short_id": {
+ "name": "short_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'unmounted'"
+ },
+ "last_error": {
+ "name": "last_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "last_health_check": {
+ "name": "last_health_check",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)"
+ },
+ "config": {
+ "name": "config",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "auto_remount": {
+ "name": "auto_remount",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": true
+ }
+ },
+ "indexes": {
+ "volumes_table_short_id_unique": {
+ "name": "volumes_table_short_id_unique",
+ "columns": [
+ "short_id"
+ ],
+ "isUnique": true
+ },
+ "volumes_table_name_unique": {
+ "name": "volumes_table_name_unique",
+ "columns": [
+ "name"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ }
+ },
+ "views": {},
+ "enums": {},
+ "_meta": {
+ "schemas": {},
+ "tables": {},
+ "columns": {}
+ },
+ "internal": {
+ "indexes": {}
+ }
+}
\ No newline at end of file
diff --git a/app/drizzle/meta/0021_snapshot.json b/app/drizzle/meta/0021_snapshot.json
new file mode 100644
index 00000000..52562dfe
--- /dev/null
+++ b/app/drizzle/meta/0021_snapshot.json
@@ -0,0 +1,823 @@
+{
+ "version": "6",
+ "dialect": "sqlite",
+ "id": "e7c02f6c-e255-402e-9f18-d50a3fef8e4d",
+ "prevId": "729d3ce9-b4b9-41f6-a270-d74c96510238",
+ "tables": {
+ "app_metadata": {
+ "name": "app_metadata",
+ "columns": {
+ "key": {
+ "name": "key",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "value": {
+ "name": "value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "backup_schedule_mirrors_table": {
+ "name": "backup_schedule_mirrors_table",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "schedule_id": {
+ "name": "schedule_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "repository_id": {
+ "name": "repository_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": true
+ },
+ "last_copy_at": {
+ "name": "last_copy_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "last_copy_status": {
+ "name": "last_copy_status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "last_copy_error": {
+ "name": "last_copy_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)"
+ }
+ },
+ "indexes": {
+ "backup_schedule_mirrors_table_schedule_id_repository_id_unique": {
+ "name": "backup_schedule_mirrors_table_schedule_id_repository_id_unique",
+ "columns": [
+ "schedule_id",
+ "repository_id"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {
+ "backup_schedule_mirrors_table_schedule_id_backup_schedules_table_id_fk": {
+ "name": "backup_schedule_mirrors_table_schedule_id_backup_schedules_table_id_fk",
+ "tableFrom": "backup_schedule_mirrors_table",
+ "tableTo": "backup_schedules_table",
+ "columnsFrom": [
+ "schedule_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "backup_schedule_mirrors_table_repository_id_repositories_table_id_fk": {
+ "name": "backup_schedule_mirrors_table_repository_id_repositories_table_id_fk",
+ "tableFrom": "backup_schedule_mirrors_table",
+ "tableTo": "repositories_table",
+ "columnsFrom": [
+ "repository_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "backup_schedule_notifications_table": {
+ "name": "backup_schedule_notifications_table",
+ "columns": {
+ "schedule_id": {
+ "name": "schedule_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "destination_id": {
+ "name": "destination_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "notify_on_start": {
+ "name": "notify_on_start",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "notify_on_success": {
+ "name": "notify_on_success",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "notify_on_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
+ },
+ "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
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)"
+ }
+ },
+ "indexes": {
+ "backup_schedules_table_name_unique": {
+ "name": "backup_schedules_table_name_unique",
+ "columns": [
+ "name"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {
+ "backup_schedules_table_volume_id_volumes_table_id_fk": {
+ "name": "backup_schedules_table_volume_id_volumes_table_id_fk",
+ "tableFrom": "backup_schedules_table",
+ "tableTo": "volumes_table",
+ "columnsFrom": [
+ "volume_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "backup_schedules_table_repository_id_repositories_table_id_fk": {
+ "name": "backup_schedules_table_repository_id_repositories_table_id_fk",
+ "tableFrom": "backup_schedules_table",
+ "tableTo": "repositories_table",
+ "columnsFrom": [
+ "repository_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "notification_destinations_table": {
+ "name": "notification_destinations_table",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": true
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "config": {
+ "name": "config",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)"
+ }
+ },
+ "indexes": {
+ "notification_destinations_table_name_unique": {
+ "name": "notification_destinations_table_name_unique",
+ "columns": [
+ "name"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "repositories_table": {
+ "name": "repositories_table",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "short_id": {
+ "name": "short_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "config": {
+ "name": "config",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "compression_mode": {
+ "name": "compression_mode",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'auto'"
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'unknown'"
+ },
+ "last_checked": {
+ "name": "last_checked",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "last_error": {
+ "name": "last_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)"
+ }
+ },
+ "indexes": {
+ "repositories_table_short_id_unique": {
+ "name": "repositories_table_short_id_unique",
+ "columns": [
+ "short_id"
+ ],
+ "isUnique": true
+ },
+ "repositories_table_name_unique": {
+ "name": "repositories_table_name_unique",
+ "columns": [
+ "name"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "sessions_table": {
+ "name": "sessions_table",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "sessions_table_user_id_users_table_id_fk": {
+ "name": "sessions_table_user_id_users_table_id_fk",
+ "tableFrom": "sessions_table",
+ "tableTo": "users_table",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "users_table": {
+ "name": "users_table",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "username": {
+ "name": "username",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "password_hash": {
+ "name": "password_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "has_downloaded_restic_password": {
+ "name": "has_downloaded_restic_password",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)"
+ }
+ },
+ "indexes": {
+ "users_table_username_unique": {
+ "name": "users_table_username_unique",
+ "columns": [
+ "username"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "volumes_table": {
+ "name": "volumes_table",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "short_id": {
+ "name": "short_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'unmounted'"
+ },
+ "last_error": {
+ "name": "last_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "last_health_check": {
+ "name": "last_health_check",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)"
+ },
+ "config": {
+ "name": "config",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "auto_remount": {
+ "name": "auto_remount",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": true
+ }
+ },
+ "indexes": {
+ "volumes_table_short_id_unique": {
+ "name": "volumes_table_short_id_unique",
+ "columns": [
+ "short_id"
+ ],
+ "isUnique": true
+ },
+ "volumes_table_name_unique": {
+ "name": "volumes_table_name_unique",
+ "columns": [
+ "name"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ }
+ },
+ "views": {},
+ "enums": {},
+ "_meta": {
+ "schemas": {},
+ "tables": {},
+ "columns": {}
+ },
+ "internal": {
+ "indexes": {}
+ }
+}
\ No newline at end of file
diff --git a/app/drizzle/meta/_journal.json b/app/drizzle/meta/_journal.json
index e11599d7..004e9b77 100644
--- a/app/drizzle/meta/_journal.json
+++ b/app/drizzle/meta/_journal.json
@@ -1,132 +1,160 @@
{
- "version": "7",
- "dialect": "sqlite",
- "entries": [
- {
- "idx": 0,
- "version": "6",
- "when": 1755765658194,
- "tag": "0000_known_madelyne_pryor",
- "breakpoints": true
- },
- {
- "idx": 1,
- "version": "6",
- "when": 1755775437391,
- "tag": "0001_far_frank_castle",
- "breakpoints": true
- },
- {
- "idx": 2,
- "version": "6",
- "when": 1756930554198,
- "tag": "0002_cheerful_randall",
- "breakpoints": true
- },
- {
- "idx": 3,
- "version": "6",
- "when": 1758653407064,
- "tag": "0003_mature_hellcat",
- "breakpoints": true
- },
- {
- "idx": 4,
- "version": "6",
- "when": 1758961535488,
- "tag": "0004_wealthy_tomas",
- "breakpoints": true
- },
- {
- "idx": 5,
- "version": "6",
- "when": 1759416698274,
- "tag": "0005_simple_alice",
- "breakpoints": true
- },
- {
- "idx": 6,
- "version": "6",
- "when": 1760734377440,
- "tag": "0006_secret_micromacro",
- "breakpoints": true
- },
- {
- "idx": 7,
- "version": "6",
- "when": 1761224911352,
- "tag": "0007_watery_sersi",
- "breakpoints": true
- },
- {
- "idx": 8,
- "version": "6",
- "when": 1761414054481,
- "tag": "0008_silent_lady_bullseye",
- "breakpoints": true
- },
- {
- "idx": 9,
- "version": "6",
- "when": 1762095226041,
- "tag": "0009_little_adam_warlock",
- "breakpoints": true
- },
- {
- "idx": 10,
- "version": "6",
- "when": 1762610065889,
- "tag": "0010_perfect_proemial_gods",
- "breakpoints": true
- },
- {
- "idx": 11,
- "version": "6",
- "when": 1763644043601,
- "tag": "0011_familiar_stone_men",
- "breakpoints": true
- },
- {
- "idx": 12,
- "version": "6",
- "when": 1764100562084,
- "tag": "0012_add_short_ids",
- "breakpoints": true
- },
- {
- "idx": 13,
- "version": "6",
- "when": 1764182159797,
- "tag": "0013_elite_sprite",
- "breakpoints": true
- },
- {
- "idx": 14,
- "version": "6",
- "when": 1764182405089,
- "tag": "0014_wild_echo",
- "breakpoints": true
- },
- {
- "idx": 15,
- "version": "6",
- "when": 1764182465287,
- "tag": "0015_jazzy_sersi",
- "breakpoints": true
- },
- {
- "idx": 16,
- "version": "6",
- "when": 1764194697035,
- "tag": "0016_fix-timestamps-to-ms",
- "breakpoints": true
- },
- {
- "idx": 17,
- "version": "6",
- "when": 1764357897219,
- "tag": "0017_fix-compression-modes",
- "breakpoints": true
- }
- ]
-}
+ "version": "7",
+ "dialect": "sqlite",
+ "entries": [
+ {
+ "idx": 0,
+ "version": "6",
+ "when": 1755765658194,
+ "tag": "0000_known_madelyne_pryor",
+ "breakpoints": true
+ },
+ {
+ "idx": 1,
+ "version": "6",
+ "when": 1755775437391,
+ "tag": "0001_far_frank_castle",
+ "breakpoints": true
+ },
+ {
+ "idx": 2,
+ "version": "6",
+ "when": 1756930554198,
+ "tag": "0002_cheerful_randall",
+ "breakpoints": true
+ },
+ {
+ "idx": 3,
+ "version": "6",
+ "when": 1758653407064,
+ "tag": "0003_mature_hellcat",
+ "breakpoints": true
+ },
+ {
+ "idx": 4,
+ "version": "6",
+ "when": 1758961535488,
+ "tag": "0004_wealthy_tomas",
+ "breakpoints": true
+ },
+ {
+ "idx": 5,
+ "version": "6",
+ "when": 1759416698274,
+ "tag": "0005_simple_alice",
+ "breakpoints": true
+ },
+ {
+ "idx": 6,
+ "version": "6",
+ "when": 1760734377440,
+ "tag": "0006_secret_micromacro",
+ "breakpoints": true
+ },
+ {
+ "idx": 7,
+ "version": "6",
+ "when": 1761224911352,
+ "tag": "0007_watery_sersi",
+ "breakpoints": true
+ },
+ {
+ "idx": 8,
+ "version": "6",
+ "when": 1761414054481,
+ "tag": "0008_silent_lady_bullseye",
+ "breakpoints": true
+ },
+ {
+ "idx": 9,
+ "version": "6",
+ "when": 1762095226041,
+ "tag": "0009_little_adam_warlock",
+ "breakpoints": true
+ },
+ {
+ "idx": 10,
+ "version": "6",
+ "when": 1762610065889,
+ "tag": "0010_perfect_proemial_gods",
+ "breakpoints": true
+ },
+ {
+ "idx": 11,
+ "version": "6",
+ "when": 1763644043601,
+ "tag": "0011_familiar_stone_men",
+ "breakpoints": true
+ },
+ {
+ "idx": 12,
+ "version": "6",
+ "when": 1764100562084,
+ "tag": "0012_add_short_ids",
+ "breakpoints": true
+ },
+ {
+ "idx": 13,
+ "version": "6",
+ "when": 1764182159797,
+ "tag": "0013_elite_sprite",
+ "breakpoints": true
+ },
+ {
+ "idx": 14,
+ "version": "6",
+ "when": 1764182405089,
+ "tag": "0014_wild_echo",
+ "breakpoints": true
+ },
+ {
+ "idx": 15,
+ "version": "6",
+ "when": 1764182465287,
+ "tag": "0015_jazzy_sersi",
+ "breakpoints": true
+ },
+ {
+ "idx": 16,
+ "version": "6",
+ "when": 1764194697035,
+ "tag": "0016_fix-timestamps-to-ms",
+ "breakpoints": true
+ },
+ {
+ "idx": 17,
+ "version": "6",
+ "when": 1764357897219,
+ "tag": "0017_fix-compression-modes",
+ "breakpoints": true
+ },
+ {
+ "idx": 18,
+ "version": "6",
+ "when": 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
+ }
+ ]
+}
\ No newline at end of file
diff --git a/app/schemas/notifications.ts b/app/schemas/notifications.ts
index 28ff5450..6f170882 100644
--- a/app/schemas/notifications.ts
+++ b/app/schemas/notifications.ts
@@ -17,8 +17,8 @@ export const emailNotificationConfigSchema = type({
type: "'email'",
smtpHost: "string",
smtpPort: "1 <= number <= 65535",
- username: "string",
- password: "string",
+ username: "string?",
+ password: "string?",
from: "string",
to: "string[]",
useTLS: "boolean",
diff --git a/app/schemas/volumes.ts b/app/schemas/volumes.ts
index dedc75f9..d0306075 100644
--- a/app/schemas/volumes.ts
+++ b/app/schemas/volumes.ts
@@ -5,6 +5,7 @@ export const BACKEND_TYPES = {
smb: "smb",
directory: "directory",
webdav: "webdav",
+ rclone: "rclone",
} as const;
export type BackendType = keyof typeof BACKEND_TYPES;
@@ -47,7 +48,14 @@ export const webdavConfigSchema = type({
ssl: "boolean?",
});
-export const volumeConfigSchema = nfsConfigSchema.or(smbConfigSchema).or(webdavConfigSchema).or(directoryConfigSchema);
+export const rcloneConfigSchema = type({
+ backend: "'rclone'",
+ remote: "string",
+ path: "string",
+ readOnly: "boolean?",
+});
+
+export const volumeConfigSchema = nfsConfigSchema.or(smbConfigSchema).or(webdavConfigSchema).or(directoryConfigSchema).or(rcloneConfigSchema);
export type BackendConfig = typeof volumeConfigSchema.infer;
diff --git a/app/server/core/capabilities.ts b/app/server/core/capabilities.ts
index 8860c416..14d62964 100644
--- a/app/server/core/capabilities.ts
+++ b/app/server/core/capabilities.ts
@@ -33,6 +33,18 @@ async function detectCapabilities(): Promise {
};
}
+export const parseDockerHost = (dockerHost?: string) => {
+ const match = dockerHost?.match(/^(ssh|http|https):\/\/([^:]+)(?::(\d+))?$/);
+ if (match) {
+ const protocol = match[1] as "ssh" | "http" | "https";
+ const host = match[2];
+ const port = match[3] ? parseInt(match[3], 10) : undefined;
+ return { protocol, host, port };
+ }
+
+ return {};
+};
+
/**
* Checks if Docker is available by:
* 1. Checking if /var/run/docker.sock exists and is accessible
@@ -40,9 +52,7 @@ async function detectCapabilities(): Promise {
*/
async function detectDocker(): Promise {
try {
- await fs.access("/var/run/docker.sock");
-
- const docker = new Docker();
+ const docker = new Docker(parseDockerHost(process.env.DOCKER_HOST));
await docker.ping();
logger.info("Docker capability: enabled");
diff --git a/app/server/core/config.ts b/app/server/core/config.ts
index 033827ed..bd138c4b 100644
--- a/app/server/core/config.ts
+++ b/app/server/core/config.ts
@@ -2,12 +2,10 @@ import { type } from "arktype";
import "dotenv/config";
const envSchema = type({
- NODE_ENV: type.enumerated("development", "production", "test").default("development"),
- SESSION_SECRET: "string?",
+ NODE_ENV: type.enumerated("development", "production", "test").default("production"),
}).pipe((s) => ({
__prod__: s.NODE_ENV === "production",
environment: s.NODE_ENV,
- sessionSecret: s.SESSION_SECRET || "change-me-in-production-please",
}));
const parseConfig = (env: unknown) => {
diff --git a/app/server/core/events.ts b/app/server/core/events.ts
index 33fbb42a..2e7f43d1 100644
--- a/app/server/core/events.ts
+++ b/app/server/core/events.ts
@@ -24,6 +24,14 @@ interface ServerEvents {
repositoryName: string;
status: "success" | "error" | "stopped" | "warning";
}) => void;
+ "mirror:started": (data: { scheduleId: number; repositoryId: string; repositoryName: string }) => void;
+ "mirror:completed": (data: {
+ scheduleId: number;
+ repositoryId: string;
+ repositoryName: string;
+ status: "success" | "error";
+ error?: string;
+ }) => void;
"volume:mounted": (data: { volumeName: string }) => void;
"volume:unmounted": (data: { volumeName: string }) => void;
"volume:updated": (data: { volumeName: string }) => void;
diff --git a/app/server/core/repository-mutex.ts b/app/server/core/repository-mutex.ts
new file mode 100644
index 00000000..4717c882
--- /dev/null
+++ b/app/server/core/repository-mutex.ts
@@ -0,0 +1,180 @@
+import { logger } from "../utils/logger";
+
+export type LockType = "shared" | "exclusive";
+
+interface LockHolder {
+ id: string;
+ operation: string;
+ acquiredAt: number;
+}
+
+interface RepositoryLockState {
+ sharedHolders: Map;
+ exclusiveHolder: LockHolder | null;
+ waitQueue: Array<{
+ type: LockType;
+ operation: string;
+ resolve: (lockId: string) => void;
+ }>;
+}
+
+class RepositoryMutex {
+ private locks = new Map();
+ private lockIdCounter = 0;
+
+ private getOrCreateState(repositoryId: string): RepositoryLockState {
+ let state = this.locks.get(repositoryId);
+ if (!state) {
+ state = {
+ sharedHolders: new Map(),
+ exclusiveHolder: null,
+ waitQueue: [],
+ };
+ this.locks.set(repositoryId, state);
+ }
+ return state;
+ }
+
+ private generateLockId(): string {
+ return `lock_${++this.lockIdCounter}_${Date.now()}`;
+ }
+
+ private cleanupStateIfEmpty(repositoryId: string): void {
+ const state = this.locks.get(repositoryId);
+ if (state && state.sharedHolders.size === 0 && !state.exclusiveHolder && state.waitQueue.length === 0) {
+ this.locks.delete(repositoryId);
+ }
+ }
+
+ async acquireShared(repositoryId: string, operation: string): Promise<() => void> {
+ const state = this.getOrCreateState(repositoryId);
+
+ if (!state.exclusiveHolder) {
+ const lockId = this.generateLockId();
+ state.sharedHolders.set(lockId, {
+ id: lockId,
+ operation,
+ acquiredAt: Date.now(),
+ });
+ return () => this.releaseShared(repositoryId, lockId);
+ }
+
+ logger.debug(
+ `[Mutex] Waiting for shared lock on repo ${repositoryId}: ${operation} (exclusive held by: ${state.exclusiveHolder.operation})`,
+ );
+ const lockId = await new Promise((resolve) => {
+ state.waitQueue.push({ type: "shared", operation, resolve });
+ });
+
+ return () => this.releaseShared(repositoryId, lockId);
+ }
+
+ async acquireExclusive(repositoryId: string, operation: string): Promise<() => void> {
+ const state = this.getOrCreateState(repositoryId);
+
+ if (!state.exclusiveHolder && state.sharedHolders.size === 0 && state.waitQueue.length === 0) {
+ const lockId = this.generateLockId();
+ state.exclusiveHolder = {
+ id: lockId,
+ operation,
+ acquiredAt: Date.now(),
+ };
+ return () => this.releaseExclusive(repositoryId, lockId);
+ }
+
+ logger.debug(
+ `[Mutex] Waiting for exclusive lock on repo ${repositoryId}: ${operation} (shared: ${state.sharedHolders.size}, exclusive: ${state.exclusiveHolder ? "yes" : "no"}, queue: ${state.waitQueue.length})`,
+ );
+ const lockId = await new Promise((resolve) => {
+ state.waitQueue.push({ type: "exclusive", operation, resolve });
+ });
+
+ logger.debug(`[Mutex] Acquired exclusive lock for repo ${repositoryId}: ${operation} (${lockId})`);
+ return () => this.releaseExclusive(repositoryId, lockId);
+ }
+
+ private releaseShared(repositoryId: string, lockId: string): void {
+ const state = this.locks.get(repositoryId);
+ if (!state) {
+ return;
+ }
+
+ const holder = state.sharedHolders.get(lockId);
+ if (!holder) {
+ return;
+ }
+
+ state.sharedHolders.delete(lockId);
+ const duration = Date.now() - holder.acquiredAt;
+ logger.debug(`[Mutex] Released shared lock for repo ${repositoryId}: ${holder.operation} (held for ${duration}ms)`);
+
+ this.processWaitQueue(repositoryId);
+ this.cleanupStateIfEmpty(repositoryId);
+ }
+
+ private releaseExclusive(repositoryId: string, lockId: string): void {
+ const state = this.locks.get(repositoryId);
+ if (!state) {
+ return;
+ }
+
+ if (!state.exclusiveHolder || state.exclusiveHolder.id !== lockId) {
+ return;
+ }
+
+ const duration = Date.now() - state.exclusiveHolder.acquiredAt;
+ logger.debug(
+ `[Mutex] Released exclusive lock for repo ${repositoryId}: ${state.exclusiveHolder.operation} (held for ${duration}ms)`,
+ );
+ state.exclusiveHolder = null;
+
+ this.processWaitQueue(repositoryId);
+ this.cleanupStateIfEmpty(repositoryId);
+ }
+
+ private processWaitQueue(repositoryId: string): void {
+ const state = this.locks.get(repositoryId);
+ if (!state || state.waitQueue.length === 0) {
+ return;
+ }
+
+ if (state.exclusiveHolder) {
+ return;
+ }
+
+ const firstWaiter = state.waitQueue[0];
+
+ if (firstWaiter.type === "exclusive") {
+ if (state.sharedHolders.size === 0) {
+ state.waitQueue.shift();
+ const lockId = this.generateLockId();
+ state.exclusiveHolder = {
+ id: lockId,
+ operation: firstWaiter.operation,
+ acquiredAt: Date.now(),
+ };
+ firstWaiter.resolve(lockId);
+ }
+ } else {
+ while (state.waitQueue.length > 0 && state.waitQueue[0].type === "shared") {
+ const waiter = state.waitQueue.shift();
+ if (!waiter) break;
+ const lockId = this.generateLockId();
+ state.sharedHolders.set(lockId, {
+ id: lockId,
+ operation: waiter.operation,
+ acquiredAt: Date.now(),
+ });
+ waiter.resolve(lockId);
+ }
+ }
+ }
+
+ isLocked(repositoryId: string): boolean {
+ const state = this.locks.get(repositoryId);
+ if (!state) return false;
+ return state.exclusiveHolder !== null || state.sharedHolders.size > 0;
+ }
+}
+
+export const repoMutex = new RepositoryMutex();
diff --git a/app/server/db/db.ts b/app/server/db/db.ts
index 78bf4bc2..07109455 100644
--- a/app/server/db/db.ts
+++ b/app/server/db/db.ts
@@ -6,6 +6,7 @@ import { migrate } from "drizzle-orm/bun-sqlite/migrator";
import { DATABASE_URL } from "../core/constants";
import * as schema from "./schema";
import fs from "node:fs/promises";
+import { config } from "../core/config";
await fs.mkdir(path.dirname(DATABASE_URL), { recursive: true });
@@ -15,8 +16,7 @@ export const db = drizzle({ client: sqlite, schema });
export const runDbMigrations = () => {
let migrationsFolder = path.join("/app", "assets", "migrations");
- const { NODE_ENV } = process.env;
- if (NODE_ENV !== "production") {
+ if (!config.__prod__) {
migrationsFolder = path.join("/app", "app", "drizzle");
}
diff --git a/app/server/db/schema.ts b/app/server/db/schema.ts
index 9b0ac0ce..55c741c2 100644
--- a/app/server/db/schema.ts
+++ b/app/server/db/schema.ts
@@ -1,5 +1,5 @@
import { relations, sql } from "drizzle-orm";
-import { int, integer, sqliteTable, text, primaryKey } from "drizzle-orm/sqlite-core";
+import { int, integer, sqliteTable, text, primaryKey, unique } from "drizzle-orm/sqlite-core";
import type { CompressionMode, RepositoryBackend, repositoryConfigSchema, RepositoryStatus } from "~/schemas/restic";
import type { BackendStatus, BackendType, volumeConfigSchema } from "~/schemas/volumes";
import type { NotificationType, notificationConfigSchema } from "~/schemas/notifications";
@@ -67,6 +67,7 @@ export type Repository = typeof repositoriesTable.$inferSelect;
*/
export const backupSchedulesTable = sqliteTable("backup_schedules_table", {
id: int().primaryKey({ autoIncrement: true }),
+ name: text().notNull().unique(),
volumeId: int("volume_id")
.notNull()
.references(() => volumesTable.id, { onDelete: "cascade" }),
@@ -85,6 +86,7 @@ export const backupSchedulesTable = sqliteTable("backup_schedules_table", {
keepWithinDuration?: string;
}>(),
excludePatterns: text("exclude_patterns", { mode: "json" }).$type().default([]),
+ excludeIfPresent: text("exclude_if_present", { mode: "json" }).$type().default([]),
includePatterns: text("include_patterns", { mode: "json" }).$type().default([]),
lastBackupAt: int("last_backup_at", { mode: "number" }),
lastBackupStatus: text("last_backup_status").$type<"success" | "error" | "in_progress" | "warning">(),
@@ -93,6 +95,7 @@ export const backupSchedulesTable = sqliteTable("backup_schedules_table", {
createdAt: int("created_at", { mode: "number" }).notNull().default(sql`(unixepoch() * 1000)`),
updatedAt: int("updated_at", { mode: "number" }).notNull().default(sql`(unixepoch() * 1000)`),
});
+
export const backupScheduleRelations = relations(backupSchedulesTable, ({ one, many }) => ({
volume: one(volumesTable, {
fields: [backupSchedulesTable.volumeId],
@@ -103,6 +106,7 @@ export const backupScheduleRelations = relations(backupSchedulesTable, ({ one, m
references: [repositoriesTable.id],
}),
notifications: many(backupScheduleNotificationsTable),
+ mirrors: many(backupScheduleMirrorsTable),
}));
export type BackupSchedule = typeof backupSchedulesTable.$inferSelect;
@@ -137,6 +141,7 @@ export const backupScheduleNotificationsTable = sqliteTable(
.references(() => notificationDestinationsTable.id, { onDelete: "cascade" }),
notifyOnStart: int("notify_on_start", { mode: "boolean" }).notNull().default(false),
notifyOnSuccess: int("notify_on_success", { mode: "boolean" }).notNull().default(false),
+ notifyOnWarning: int("notify_on_warning", { mode: "boolean" }).notNull().default(true),
notifyOnFailure: int("notify_on_failure", { mode: "boolean" }).notNull().default(true),
createdAt: int("created_at", { mode: "number" }).notNull().default(sql`(unixepoch() * 1000)`),
},
@@ -154,6 +159,41 @@ export const backupScheduleNotificationRelations = relations(backupScheduleNotif
}));
export type BackupScheduleNotification = typeof backupScheduleNotificationsTable.$inferSelect;
+/**
+ * Backup Schedule Mirrors Junction Table (Many-to-Many)
+ * Allows copying snapshots to secondary repositories after backup completes
+ */
+export const backupScheduleMirrorsTable = sqliteTable(
+ "backup_schedule_mirrors_table",
+ {
+ id: int().primaryKey({ autoIncrement: true }),
+ scheduleId: int("schedule_id")
+ .notNull()
+ .references(() => backupSchedulesTable.id, { onDelete: "cascade" }),
+ repositoryId: text("repository_id")
+ .notNull()
+ .references(() => repositoriesTable.id, { onDelete: "cascade" }),
+ enabled: int("enabled", { mode: "boolean" }).notNull().default(true),
+ lastCopyAt: int("last_copy_at", { mode: "number" }),
+ lastCopyStatus: text("last_copy_status").$type<"success" | "error">(),
+ lastCopyError: text("last_copy_error"),
+ createdAt: int("created_at", { mode: "number" }).notNull().default(sql`(unixepoch() * 1000)`),
+ },
+ (table) => [unique().on(table.scheduleId, table.repositoryId)],
+);
+
+export const backupScheduleMirrorRelations = relations(backupScheduleMirrorsTable, ({ one }) => ({
+ schedule: one(backupSchedulesTable, {
+ fields: [backupScheduleMirrorsTable.scheduleId],
+ references: [backupSchedulesTable.id],
+ }),
+ repository: one(repositoriesTable, {
+ fields: [backupScheduleMirrorsTable.repositoryId],
+ references: [repositoriesTable.id],
+ }),
+}));
+export type BackupScheduleMirror = typeof backupScheduleMirrorsTable.$inferSelect;
+
/**
* App Metadata Table
* Used for storing key-value pairs like migration checkpoints
diff --git a/app/server/jobs/repository-healthchecks.ts b/app/server/jobs/repository-healthchecks.ts
index 1e600c9f..bc233903 100644
--- a/app/server/jobs/repository-healthchecks.ts
+++ b/app/server/jobs/repository-healthchecks.ts
@@ -4,6 +4,7 @@ import { logger } from "../utils/logger";
import { db } from "../db/db";
import { eq, or } from "drizzle-orm";
import { repositoriesTable } from "../db/schema";
+import { repoMutex } from "../core/repository-mutex";
export class RepositoryHealthCheckJob extends Job {
async run() {
@@ -14,6 +15,11 @@ export class RepositoryHealthCheckJob extends Job {
});
for (const repository of repositories) {
+ if (repoMutex.isLocked(repository.id)) {
+ logger.debug(`Skipping health check for repository ${repository.name}: currently locked`);
+ continue;
+ }
+
try {
await repositoriesService.checkHealth(repository.id);
} catch (error) {
diff --git a/app/server/modules/backends/backend.ts b/app/server/modules/backends/backend.ts
index c3431d5f..c19388d3 100644
--- a/app/server/modules/backends/backend.ts
+++ b/app/server/modules/backends/backend.ts
@@ -3,6 +3,7 @@ import type { Volume } from "../../db/schema";
import { getVolumePath } from "../volumes/helpers";
import { makeDirectoryBackend } from "./directory/directory-backend";
import { makeNfsBackend } from "./nfs/nfs-backend";
+import { makeRcloneBackend } from "./rclone/rclone-backend";
import { makeSmbBackend } from "./smb/smb-backend";
import { makeWebdavBackend } from "./webdav/webdav-backend";
@@ -33,5 +34,8 @@ export const createVolumeBackend = (volume: Volume): VolumeBackend => {
case "webdav": {
return makeWebdavBackend(volume.config, path);
}
+ case "rclone": {
+ return makeRcloneBackend(volume.config, path);
+ }
}
};
diff --git a/app/server/modules/backends/rclone/rclone-backend.ts b/app/server/modules/backends/rclone/rclone-backend.ts
new file mode 100644
index 00000000..0fcffba8
--- /dev/null
+++ b/app/server/modules/backends/rclone/rclone-backend.ts
@@ -0,0 +1,128 @@
+import * as fs from "node:fs/promises";
+import * as os from "node:os";
+import { $ } from "bun";
+import { OPERATION_TIMEOUT } from "../../../core/constants";
+import { toMessage } from "../../../utils/errors";
+import { logger } from "../../../utils/logger";
+import { getMountForPath } from "../../../utils/mountinfo";
+import { withTimeout } from "../../../utils/timeout";
+import type { VolumeBackend } from "../backend";
+import { executeUnmount } from "../utils/backend-utils";
+import { BACKEND_STATUS, type BackendConfig } from "~/schemas/volumes";
+
+const mount = async (config: BackendConfig, path: string) => {
+ logger.debug(`Mounting rclone volume ${path}...`);
+
+ if (config.backend !== "rclone") {
+ logger.error("Provided config is not for rclone backend");
+ return { status: BACKEND_STATUS.error, error: "Provided config is not for rclone backend" };
+ }
+
+ if (os.platform() !== "linux") {
+ logger.error("Rclone mounting is only supported on Linux hosts.");
+ return { status: BACKEND_STATUS.error, error: "Rclone mounting is only supported on Linux hosts." };
+ }
+
+ const { status } = await checkHealth(path);
+ if (status === "mounted") {
+ return { status: BACKEND_STATUS.mounted };
+ }
+
+ logger.debug(`Trying to unmount any existing mounts at ${path} before mounting...`);
+ await unmount(path);
+
+ const run = async () => {
+ await fs.mkdir(path, { recursive: true });
+
+ const remotePath = `${config.remote}:${config.path}`;
+ const args = ["mount", remotePath, path, "--daemon"];
+
+ if (config.readOnly) {
+ args.push("--read-only");
+ }
+
+ args.push("--vfs-cache-mode", "writes");
+ args.push("--allow-non-empty");
+ args.push("--allow-other");
+
+ logger.debug(`Mounting rclone volume ${path}...`);
+ logger.info(`Executing rclone: rclone ${args.join(" ")}`);
+
+ const result = await $`rclone ${args}`.nothrow();
+
+ if (result.exitCode !== 0) {
+ const errorMsg = result.stderr.toString() || result.stdout.toString() || "Unknown error";
+ throw new Error(`Failed to mount rclone volume: ${errorMsg}`);
+ }
+
+ logger.info(`Rclone volume at ${path} mounted successfully.`);
+ return { status: BACKEND_STATUS.mounted };
+ };
+
+ try {
+ return await withTimeout(run(), OPERATION_TIMEOUT, "Rclone mount");
+ } catch (error) {
+ const errorMsg = toMessage(error);
+
+ logger.error("Error mounting rclone volume", { error: errorMsg });
+ return { status: BACKEND_STATUS.error, error: errorMsg };
+ }
+};
+
+const unmount = async (path: string) => {
+ if (os.platform() !== "linux") {
+ logger.error("Rclone unmounting is only supported on Linux hosts.");
+ return { status: BACKEND_STATUS.error, error: "Rclone unmounting is only supported on Linux hosts." };
+ }
+
+ const run = async () => {
+ try {
+ await fs.access(path);
+ } catch (e) {
+ logger.warn(`Path ${path} does not exist. Skipping unmount.`, e);
+ return { status: BACKEND_STATUS.unmounted };
+ }
+
+ await executeUnmount(path);
+ await fs.rmdir(path);
+
+ logger.info(`Rclone volume at ${path} unmounted successfully.`);
+ return { status: BACKEND_STATUS.unmounted };
+ };
+
+ try {
+ return await withTimeout(run(), OPERATION_TIMEOUT, "Rclone unmount");
+ } catch (error) {
+ logger.error("Error unmounting rclone volume", { path, error: toMessage(error) });
+ return { status: BACKEND_STATUS.error, error: toMessage(error) };
+ }
+};
+
+const checkHealth = async (path: string) => {
+ const run = async () => {
+ logger.debug(`Checking health of rclone volume at ${path}...`);
+ await fs.access(path);
+
+ const mount = await getMountForPath(path);
+
+ if (!mount || mount.fstype !== "fuse.rclone") {
+ throw new Error(`Path ${path} is not mounted as rclone.`);
+ }
+
+ logger.debug(`Rclone volume at ${path} is healthy and mounted.`);
+ return { status: BACKEND_STATUS.mounted };
+ };
+
+ try {
+ return await withTimeout(run(), OPERATION_TIMEOUT, "Rclone health check");
+ } catch (error) {
+ logger.error("Rclone volume health check failed:", toMessage(error));
+ return { status: BACKEND_STATUS.error, error: toMessage(error) };
+ }
+};
+
+export const makeRcloneBackend = (config: BackendConfig, path: string): VolumeBackend => ({
+ mount: () => mount(config, path),
+ unmount: () => unmount(path),
+ checkHealth: () => checkHealth(path),
+});
diff --git a/app/server/modules/backends/smb/smb-backend.ts b/app/server/modules/backends/smb/smb-backend.ts
index cdc112ab..774e10fd 100644
--- a/app/server/modules/backends/smb/smb-backend.ts
+++ b/app/server/modules/backends/smb/smb-backend.ts
@@ -1,6 +1,7 @@
import * as fs from "node:fs/promises";
import * as os from "node:os";
import { OPERATION_TIMEOUT } from "../../../core/constants";
+import { cryptoUtils } from "../../../utils/crypto";
import { toMessage } from "../../../utils/errors";
import { logger } from "../../../utils/logger";
import { getMountForPath } from "../../../utils/mountinfo";
@@ -33,10 +34,12 @@ const mount = async (config: BackendConfig, path: string) => {
const run = async () => {
await fs.mkdir(path, { recursive: true });
+ const password = await cryptoUtils.decrypt(config.password);
+
const source = `//${config.server}/${config.share}`;
const options = [
`user=${config.username}`,
- `pass=${config.password}`,
+ `pass=${password}`,
`vers=${config.vers}`,
`port=${config.port}`,
"uid=1000",
diff --git a/app/server/modules/backends/webdav/webdav-backend.ts b/app/server/modules/backends/webdav/webdav-backend.ts
index 1e9b72f6..2467736d 100644
--- a/app/server/modules/backends/webdav/webdav-backend.ts
+++ b/app/server/modules/backends/webdav/webdav-backend.ts
@@ -3,6 +3,7 @@ import * as fs from "node:fs/promises";
import * as os from "node:os";
import { promisify } from "node:util";
import { OPERATION_TIMEOUT } from "../../../core/constants";
+import { cryptoUtils } from "../../../utils/crypto";
import { toMessage } from "../../../utils/errors";
import { logger } from "../../../utils/logger";
import { getMountForPath } from "../../../utils/mountinfo";
@@ -49,8 +50,9 @@ const mount = async (config: BackendConfig, path: string) => {
: ["uid=1000", "gid=1000", "file_mode=0664", "dir_mode=0775"];
if (config.username && config.password) {
+ const password = await cryptoUtils.decrypt(config.password);
const secretsFile = "/etc/davfs2/secrets";
- const secretsContent = `${source} ${config.username} ${config.password}\n`;
+ const secretsContent = `${source} ${config.username} ${password}\n`;
await fs.appendFile(secretsFile, secretsContent, { mode: 0o600 });
}
diff --git a/app/server/modules/backups/backups.controller.ts b/app/server/modules/backups/backups.controller.ts
index ed6fa4a9..0bcc93eb 100644
--- a/app/server/modules/backups/backups.controller.ts
+++ b/app/server/modules/backups/backups.controller.ts
@@ -12,6 +12,10 @@ import {
stopBackupDto,
updateBackupScheduleDto,
updateBackupScheduleBody,
+ getScheduleMirrorsDto,
+ updateScheduleMirrorsDto,
+ updateScheduleMirrorsBody,
+ getMirrorCompatibilityDto,
type CreateBackupScheduleDto,
type DeleteBackupScheduleDto,
type GetBackupScheduleDto,
@@ -21,6 +25,9 @@ import {
type RunForgetDto,
type StopBackupDto,
type UpdateBackupScheduleDto,
+ type GetScheduleMirrorsDto,
+ type UpdateScheduleMirrorsDto,
+ type GetMirrorCompatibilityDto,
} from "./backups.dto";
import { backupsService } from "./backups.service";
import {
@@ -113,4 +120,23 @@ export const backupScheduleController = new Hono()
return c.json(assignments, 200);
},
- );
+ )
+ .get("/:scheduleId/mirrors", getScheduleMirrorsDto, async (c) => {
+ const scheduleId = Number.parseInt(c.req.param("scheduleId"), 10);
+ const mirrors = await backupsService.getMirrors(scheduleId);
+
+ return c.json(mirrors, 200);
+ })
+ .put("/:scheduleId/mirrors", updateScheduleMirrorsDto, validator("json", updateScheduleMirrorsBody), async (c) => {
+ const scheduleId = Number.parseInt(c.req.param("scheduleId"), 10);
+ const body = c.req.valid("json");
+ const mirrors = await backupsService.updateMirrors(scheduleId, body);
+
+ return c.json(mirrors, 200);
+ })
+ .get("/:scheduleId/mirrors/compatibility", getMirrorCompatibilityDto, async (c) => {
+ const scheduleId = Number.parseInt(c.req.param("scheduleId"), 10);
+ const compatibility = await backupsService.getMirrorCompatibility(scheduleId);
+
+ return c.json(compatibility, 200);
+ });
diff --git a/app/server/modules/backups/backups.dto.ts b/app/server/modules/backups/backups.dto.ts
index 95c4d49c..fc771bf1 100644
--- a/app/server/modules/backups/backups.dto.ts
+++ b/app/server/modules/backups/backups.dto.ts
@@ -17,12 +17,14 @@ export type RetentionPolicy = typeof retentionPolicySchema.infer;
const backupScheduleSchema = type({
id: "number",
+ name: "string",
volumeId: "number",
repositoryId: "string",
enabled: "boolean",
cronExpression: "string",
retentionPolicy: retentionPolicySchema.or("null"),
excludePatterns: "string[] | null",
+ excludeIfPresent: "string[] | null",
includePatterns: "string[] | null",
lastBackupAt: "number | null",
lastBackupStatus: "'success' | 'error' | 'in_progress' | 'warning' | null",
@@ -37,6 +39,19 @@ const backupScheduleSchema = type({
}),
);
+const scheduleMirrorSchema = type({
+ scheduleId: "number",
+ repositoryId: "string",
+ enabled: "boolean",
+ lastCopyAt: "number | null",
+ lastCopyStatus: "'success' | 'error' | null",
+ lastCopyError: "string | null",
+ createdAt: "number",
+ repository: repositorySchema,
+});
+
+export type ScheduleMirrorDto = typeof scheduleMirrorSchema.infer;
+
/**
* List all backup schedules
*/
@@ -107,12 +122,14 @@ export const getBackupScheduleForVolumeDto = describeRoute({
* Create a new backup schedule
*/
export const createBackupScheduleBody = type({
+ name: "1 <= string <= 32",
volumeId: "number",
repositoryId: "string",
enabled: "boolean",
cronExpression: "string",
retentionPolicy: retentionPolicySchema.optional(),
excludePatterns: "string[]?",
+ excludeIfPresent: "string[]?",
includePatterns: "string[]?",
tags: "string[]?",
});
@@ -143,11 +160,13 @@ export const createBackupScheduleDto = describeRoute({
* Update a backup schedule
*/
export const updateBackupScheduleBody = type({
+ name: "(1 <= string <= 32)?",
repositoryId: "string",
enabled: "boolean?",
cronExpression: "string",
retentionPolicy: retentionPolicySchema.optional(),
excludePatterns: "string[]?",
+ excludeIfPresent: "string[]?",
includePatterns: "string[]?",
tags: "string[]?",
});
@@ -276,3 +295,75 @@ export const runForgetDto = describeRoute({
},
},
});
+
+export const getScheduleMirrorsResponse = scheduleMirrorSchema.array();
+export type GetScheduleMirrorsDto = typeof getScheduleMirrorsResponse.infer;
+
+export const getScheduleMirrorsDto = describeRoute({
+ description: "Get mirror repository assignments for a backup schedule",
+ operationId: "getScheduleMirrors",
+ tags: ["Backups"],
+ responses: {
+ 200: {
+ description: "List of mirror repository assignments for the schedule",
+ content: {
+ "application/json": {
+ schema: resolver(getScheduleMirrorsResponse),
+ },
+ },
+ },
+ },
+});
+
+export const updateScheduleMirrorsBody = type({
+ mirrors: type({
+ repositoryId: "string",
+ enabled: "boolean",
+ }).array(),
+});
+
+export type UpdateScheduleMirrorsBody = typeof updateScheduleMirrorsBody.infer;
+
+export const updateScheduleMirrorsResponse = scheduleMirrorSchema.array();
+export type UpdateScheduleMirrorsDto = typeof updateScheduleMirrorsResponse.infer;
+
+export const updateScheduleMirrorsDto = describeRoute({
+ description: "Update mirror repository assignments for a backup schedule",
+ operationId: "updateScheduleMirrors",
+ tags: ["Backups"],
+ responses: {
+ 200: {
+ description: "Mirror assignments updated successfully",
+ content: {
+ "application/json": {
+ schema: resolver(updateScheduleMirrorsResponse),
+ },
+ },
+ },
+ },
+});
+
+const mirrorCompatibilitySchema = type({
+ repositoryId: "string",
+ compatible: "boolean",
+ reason: "string | null",
+});
+
+export const getMirrorCompatibilityResponse = mirrorCompatibilitySchema.array();
+export type GetMirrorCompatibilityDto = typeof getMirrorCompatibilityResponse.infer;
+
+export const getMirrorCompatibilityDto = describeRoute({
+ description: "Get mirror compatibility info for all repositories relative to a backup schedule's primary repository",
+ operationId: "getMirrorCompatibility",
+ tags: ["Backups"],
+ responses: {
+ 200: {
+ description: "List of repositories with their mirror compatibility status",
+ content: {
+ "application/json": {
+ schema: resolver(getMirrorCompatibilityResponse),
+ },
+ },
+ },
+ },
+});
diff --git a/app/server/modules/backups/backups.service.ts b/app/server/modules/backups/backups.service.ts
index 89aa3ade..a95fc738 100644
--- a/app/server/modules/backups/backups.service.ts
+++ b/app/server/modules/backups/backups.service.ts
@@ -1,16 +1,18 @@
-import { eq } from "drizzle-orm";
+import { and, eq, ne } from "drizzle-orm";
import cron from "node-cron";
import { CronExpressionParser } from "cron-parser";
import { NotFoundError, BadRequestError, ConflictError } from "http-errors-enhanced";
import { db } from "../../db/db";
-import { backupSchedulesTable, repositoriesTable, volumesTable } from "../../db/schema";
+import { backupSchedulesTable, backupScheduleMirrorsTable, repositoriesTable, volumesTable } from "../../db/schema";
import { restic } from "../../utils/restic";
import { logger } from "../../utils/logger";
import { getVolumePath } from "../volumes/helpers";
-import type { CreateBackupScheduleBody, UpdateBackupScheduleBody } from "./backups.dto";
+import type { CreateBackupScheduleBody, UpdateBackupScheduleBody, UpdateScheduleMirrorsBody } from "./backups.dto";
import { toMessage } from "../../utils/errors";
import { serverEvents } from "../../core/events";
import { notificationsService } from "../notifications/notifications.service";
+import { repoMutex } from "../../core/repository-mutex";
+import { checkMirrorCompatibility, getIncompatibleMirrorError } from "~/server/utils/backend-compatibility";
const runningBackups = new Map();
@@ -42,7 +44,7 @@ const listSchedules = async () => {
const getSchedule = async (scheduleId: number) => {
const schedule = await db.query.backupSchedulesTable.findFirst({
- where: eq(volumesTable.id, scheduleId),
+ where: eq(backupSchedulesTable.id, scheduleId),
with: {
volume: true,
repository: true,
@@ -61,6 +63,14 @@ const createSchedule = async (data: CreateBackupScheduleBody) => {
throw new BadRequestError("Invalid cron expression");
}
+ const existingName = await db.query.backupSchedulesTable.findFirst({
+ where: eq(backupSchedulesTable.name, data.name),
+ });
+
+ if (existingName) {
+ throw new ConflictError("A backup schedule with this name already exists");
+ }
+
const volume = await db.query.volumesTable.findFirst({
where: eq(volumesTable.id, data.volumeId),
});
@@ -82,12 +92,14 @@ const createSchedule = async (data: CreateBackupScheduleBody) => {
const [newSchedule] = await db
.insert(backupSchedulesTable)
.values({
+ name: data.name,
volumeId: data.volumeId,
repositoryId: data.repositoryId,
enabled: data.enabled,
cronExpression: data.cronExpression,
retentionPolicy: data.retentionPolicy ?? null,
excludePatterns: data.excludePatterns ?? [],
+ excludeIfPresent: data.excludeIfPresent ?? [],
includePatterns: data.includePatterns ?? [],
nextBackupAt: nextBackupAt,
})
@@ -113,6 +125,16 @@ const updateSchedule = async (scheduleId: number, data: UpdateBackupScheduleBody
throw new BadRequestError("Invalid cron expression");
}
+ if (data.name) {
+ const existingName = await db.query.backupSchedulesTable.findFirst({
+ where: and(eq(backupSchedulesTable.name, data.name), ne(backupSchedulesTable.id, scheduleId)),
+ });
+
+ if (existingName) {
+ throw new ConflictError("A backup schedule with this name already exists");
+ }
+ }
+
const repository = await db.query.repositoriesTable.findFirst({
where: eq(repositoriesTable.id, data.repositoryId),
});
@@ -225,6 +247,7 @@ const executeBackup = async (scheduleId: number, manual = false) => {
const backupOptions: {
exclude?: string[];
+ excludeIfPresent?: string[];
include?: string[];
tags?: string[];
signal?: AbortSignal;
@@ -237,40 +260,57 @@ const executeBackup = async (scheduleId: number, manual = false) => {
backupOptions.exclude = schedule.excludePatterns;
}
+ if (schedule.excludeIfPresent && schedule.excludeIfPresent.length > 0) {
+ backupOptions.excludeIfPresent = schedule.excludeIfPresent;
+ }
+
if (schedule.includePatterns && schedule.includePatterns.length > 0) {
backupOptions.include = schedule.includePatterns;
}
- const { exitCode } = await restic.backup(repository.config, volumePath, {
- ...backupOptions,
- compressionMode: repository.compressionMode ?? "auto",
- onProgress: (progress) => {
- serverEvents.emit("backup:progress", {
- scheduleId,
- volumeName: volume.name,
- repositoryName: repository.name,
- ...progress,
- });
- },
- });
+ const releaseBackupLock = await repoMutex.acquireShared(repository.id, `backup:${volume.name}`);
+ let exitCode: number;
+ try {
+ const result = await restic.backup(repository.config, volumePath, {
+ ...backupOptions,
+ compressionMode: repository.compressionMode ?? "auto",
+ onProgress: (progress) => {
+ serverEvents.emit("backup:progress", {
+ scheduleId,
+ volumeName: volume.name,
+ repositoryName: repository.name,
+ ...progress,
+ });
+ },
+ });
+ exitCode = result.exitCode;
+ } finally {
+ releaseBackupLock();
+ }
if (schedule.retentionPolicy) {
- await restic.forget(repository.config, schedule.retentionPolicy, { tag: schedule.id.toString() });
+ void runForget(schedule.id);
}
+ copyToMirrors(scheduleId, repository, schedule.retentionPolicy).catch((error) => {
+ logger.error(`Background mirror copy failed for schedule ${scheduleId}: ${toMessage(error)}`);
+ });
+
+ const finalStatus = exitCode === 0 ? "success" : "warning";
+
const nextBackupAt = calculateNextRun(schedule.cronExpression);
await db
.update(backupSchedulesTable)
.set({
lastBackupAt: Date.now(),
- lastBackupStatus: exitCode === 0 ? "success" : "warning",
+ lastBackupStatus: finalStatus,
lastBackupError: null,
nextBackupAt: nextBackupAt,
updatedAt: Date.now(),
})
.where(eq(backupSchedulesTable.id, scheduleId));
- if (exitCode !== 0) {
+ if (finalStatus === "warning") {
logger.warn(`Backup completed with warnings for volume ${volume.name} to repository ${repository.name}`);
} else {
logger.info(`Backup completed successfully for volume ${volume.name} to repository ${repository.name}`);
@@ -280,11 +320,11 @@ const executeBackup = async (scheduleId: number, manual = false) => {
scheduleId,
volumeName: volume.name,
repositoryName: repository.name,
- status: exitCode === 0 ? "success" : "warning",
+ status: finalStatus,
});
notificationsService
- .sendBackupNotification(scheduleId, exitCode === 0 ? "success" : "warning", {
+ .sendBackupNotification(scheduleId, finalStatus === "success" ? "success" : "warning", {
volumeName: volume.name,
repositoryName: repository.name,
})
@@ -402,11 +442,202 @@ const runForget = async (scheduleId: number) => {
throw new NotFoundError("Repository not found");
}
- logger.info(`Manually running retention policy (forget) for schedule ${scheduleId}`);
- await restic.forget(repository.config, schedule.retentionPolicy, { tag: schedule.id.toString() });
+ logger.info(`running retention policy (forget) for schedule ${scheduleId}`);
+ const releaseLock = await repoMutex.acquireExclusive(repository.id, `forget:manual:${scheduleId}`);
+ try {
+ await restic.forget(repository.config, schedule.retentionPolicy, { tag: schedule.id.toString() });
+ } finally {
+ releaseLock();
+ }
+
logger.info(`Retention policy applied successfully for schedule ${scheduleId}`);
};
+const getMirrors = async (scheduleId: number) => {
+ const schedule = await db.query.backupSchedulesTable.findFirst({
+ where: eq(backupSchedulesTable.id, scheduleId),
+ });
+
+ if (!schedule) {
+ throw new NotFoundError("Backup schedule not found");
+ }
+
+ const mirrors = await db.query.backupScheduleMirrorsTable.findMany({
+ where: eq(backupScheduleMirrorsTable.scheduleId, scheduleId),
+ with: { repository: true },
+ });
+
+ return mirrors;
+};
+
+const updateMirrors = async (scheduleId: number, data: UpdateScheduleMirrorsBody) => {
+ const schedule = await db.query.backupSchedulesTable.findFirst({
+ where: eq(backupSchedulesTable.id, scheduleId),
+ with: { repository: true },
+ });
+
+ if (!schedule) {
+ throw new NotFoundError("Backup schedule not found");
+ }
+
+ for (const mirror of data.mirrors) {
+ if (mirror.repositoryId === schedule.repositoryId) {
+ throw new BadRequestError("Cannot add the primary repository as a mirror");
+ }
+
+ const repo = await db.query.repositoriesTable.findFirst({
+ where: eq(repositoriesTable.id, mirror.repositoryId),
+ });
+
+ if (!repo) {
+ throw new NotFoundError(`Repository ${mirror.repositoryId} not found`);
+ }
+
+ const compatibility = await checkMirrorCompatibility(schedule.repository.config, repo.config, repo.id);
+
+ if (!compatibility.compatible) {
+ throw new BadRequestError(
+ getIncompatibleMirrorError(repo.name, schedule.repository.config.backend, repo.config.backend),
+ );
+ }
+ }
+
+ const existingMirrors = await db.query.backupScheduleMirrorsTable.findMany({
+ where: eq(backupScheduleMirrorsTable.scheduleId, scheduleId),
+ });
+
+ const existingMirrorsMap = new Map(
+ existingMirrors.map((m) => [
+ m.repositoryId,
+ { lastCopyAt: m.lastCopyAt, lastCopyStatus: m.lastCopyStatus, lastCopyError: m.lastCopyError },
+ ]),
+ );
+
+ await db.delete(backupScheduleMirrorsTable).where(eq(backupScheduleMirrorsTable.scheduleId, scheduleId));
+
+ if (data.mirrors.length > 0) {
+ await db.insert(backupScheduleMirrorsTable).values(
+ data.mirrors.map((mirror) => {
+ const existing = existingMirrorsMap.get(mirror.repositoryId);
+ return {
+ scheduleId,
+ repositoryId: mirror.repositoryId,
+ enabled: mirror.enabled,
+ lastCopyAt: existing?.lastCopyAt ?? null,
+ lastCopyStatus: existing?.lastCopyStatus ?? null,
+ lastCopyError: existing?.lastCopyError ?? null,
+ };
+ }),
+ );
+ }
+
+ return getMirrors(scheduleId);
+};
+
+const copyToMirrors = async (
+ scheduleId: number,
+ sourceRepository: { id: string; config: (typeof repositoriesTable.$inferSelect)["config"] },
+ retentionPolicy: (typeof backupSchedulesTable.$inferSelect)["retentionPolicy"],
+) => {
+ const mirrors = await db.query.backupScheduleMirrorsTable.findMany({
+ where: eq(backupScheduleMirrorsTable.scheduleId, scheduleId),
+ with: { repository: true },
+ });
+
+ const enabledMirrors = mirrors.filter((m) => m.enabled);
+
+ if (enabledMirrors.length === 0) {
+ return;
+ }
+
+ logger.info(
+ `[Background] Copying snapshots to ${enabledMirrors.length} mirror repositories for schedule ${scheduleId}`,
+ );
+
+ for (const mirror of enabledMirrors) {
+ try {
+ logger.info(`[Background] Copying to mirror repository: ${mirror.repository.name}`);
+
+ serverEvents.emit("mirror:started", {
+ scheduleId,
+ repositoryId: mirror.repositoryId,
+ repositoryName: mirror.repository.name,
+ });
+
+ const releaseSource = await repoMutex.acquireShared(sourceRepository.id, `mirror_source:${scheduleId}`);
+ const releaseMirror = await repoMutex.acquireShared(mirror.repository.id, `mirror:${scheduleId}`);
+
+ try {
+ await restic.copy(sourceRepository.config, mirror.repository.config, { tag: scheduleId.toString() });
+ } finally {
+ releaseSource();
+ releaseMirror();
+ }
+
+ if (retentionPolicy) {
+ const releaseForget = await repoMutex.acquireExclusive(mirror.repository.id, `forget:mirror:${scheduleId}`);
+
+ try {
+ logger.info(`[Background] Applying retention policy to mirror repository: ${mirror.repository.name}`);
+ await restic.forget(mirror.repository.config, retentionPolicy, { tag: scheduleId.toString() });
+ } finally {
+ releaseForget();
+ }
+ }
+
+ await db
+ .update(backupScheduleMirrorsTable)
+ .set({ lastCopyAt: Date.now(), lastCopyStatus: "success", lastCopyError: null })
+ .where(eq(backupScheduleMirrorsTable.id, mirror.id));
+
+ logger.info(`[Background] Successfully copied to mirror repository: ${mirror.repository.name}`);
+
+ serverEvents.emit("mirror:completed", {
+ scheduleId,
+ repositoryId: mirror.repositoryId,
+ repositoryName: mirror.repository.name,
+ status: "success",
+ });
+ } catch (error) {
+ const errorMessage = toMessage(error);
+ logger.error(`[Background] Failed to copy to mirror repository ${mirror.repository.name}: ${errorMessage}`);
+
+ await db
+ .update(backupScheduleMirrorsTable)
+ .set({ lastCopyAt: Date.now(), lastCopyStatus: "error", lastCopyError: errorMessage })
+ .where(eq(backupScheduleMirrorsTable.id, mirror.id));
+
+ serverEvents.emit("mirror:completed", {
+ scheduleId,
+ repositoryId: mirror.repositoryId,
+ repositoryName: mirror.repository.name,
+ status: "error",
+ error: errorMessage,
+ });
+ }
+ }
+};
+
+const getMirrorCompatibility = async (scheduleId: number) => {
+ const schedule = await db.query.backupSchedulesTable.findFirst({
+ where: eq(backupSchedulesTable.id, scheduleId),
+ with: { repository: true },
+ });
+
+ if (!schedule) {
+ throw new NotFoundError("Backup schedule not found");
+ }
+
+ const allRepositories = await db.query.repositoriesTable.findMany();
+ const repos = allRepositories.filter((repo) => repo.id !== schedule.repositoryId);
+
+ const compatibility = await Promise.all(
+ repos.map((repo) => checkMirrorCompatibility(schedule.repository.config, repo.config, repo.id)),
+ );
+
+ return compatibility;
+};
+
export const backupsService = {
listSchedules,
getSchedule,
@@ -418,4 +649,7 @@ export const backupsService = {
getScheduleForVolume,
stopBackup,
runForget,
+ getMirrors,
+ updateMirrors,
+ getMirrorCompatibility,
};
diff --git a/app/server/modules/events/events.controller.ts b/app/server/modules/events/events.controller.ts
index a93dc27e..cd40b845 100644
--- a/app/server/modules/events/events.controller.ts
+++ b/app/server/modules/events/events.controller.ts
@@ -70,12 +70,34 @@ export const eventsController = new Hono().get("/", (c) => {
});
};
+ const onMirrorStarted = (data: { scheduleId: number; repositoryId: string; repositoryName: string }) => {
+ stream.writeSSE({
+ data: JSON.stringify(data),
+ event: "mirror:started",
+ });
+ };
+
+ const onMirrorCompleted = (data: {
+ scheduleId: number;
+ repositoryId: string;
+ repositoryName: string;
+ status: "success" | "error";
+ error?: string;
+ }) => {
+ stream.writeSSE({
+ data: JSON.stringify(data),
+ event: "mirror:completed",
+ });
+ };
+
serverEvents.on("backup:started", onBackupStarted);
serverEvents.on("backup:progress", onBackupProgress);
serverEvents.on("backup:completed", onBackupCompleted);
serverEvents.on("volume:mounted", onVolumeMounted);
serverEvents.on("volume:unmounted", onVolumeUnmounted);
serverEvents.on("volume:updated", onVolumeUpdated);
+ serverEvents.on("mirror:started", onMirrorStarted);
+ serverEvents.on("mirror:completed", onMirrorCompleted);
let keepAlive = true;
@@ -88,6 +110,8 @@ export const eventsController = new Hono().get("/", (c) => {
serverEvents.off("volume:mounted", onVolumeMounted);
serverEvents.off("volume:unmounted", onVolumeUnmounted);
serverEvents.off("volume:updated", onVolumeUpdated);
+ serverEvents.off("mirror:started", onMirrorStarted);
+ serverEvents.off("mirror:completed", onMirrorCompleted);
});
while (keepAlive) {
diff --git a/app/server/modules/lifecycle/startup.ts b/app/server/modules/lifecycle/startup.ts
index ea5b6f98..105071a2 100644
--- a/app/server/modules/lifecycle/startup.ts
+++ b/app/server/modules/lifecycle/startup.ts
@@ -34,7 +34,7 @@ export const startup = async () => {
Scheduler.build(CleanupDanglingMountsJob).schedule("0 * * * *");
Scheduler.build(VolumeHealthCheckJob).schedule("*/30 * * * *");
- Scheduler.build(RepositoryHealthCheckJob).schedule("0 12 * * *");
+ Scheduler.build(RepositoryHealthCheckJob).schedule("50 12 * * *");
Scheduler.build(BackupExecutionJob).schedule("* * * * *");
Scheduler.build(CleanupSessionsJob).schedule("0 0 * * *");
};
diff --git a/app/server/modules/notifications/builders/email.ts b/app/server/modules/notifications/builders/email.ts
index a0323f10..cec160dd 100644
--- a/app/server/modules/notifications/builders/email.ts
+++ b/app/server/modules/notifications/builders/email.ts
@@ -1,10 +1,13 @@
import type { NotificationConfig } from "~/schemas/notifications";
export function buildEmailShoutrrrUrl(config: Extract): string {
- const auth = `${encodeURIComponent(config.username)}:${encodeURIComponent(config.password)}`;
+ const auth =
+ config.username && config.password
+ ? `${encodeURIComponent(config.username)}:${encodeURIComponent(config.password)}@`
+ : "";
const host = `${config.smtpHost}:${config.smtpPort}`;
const toRecipients = config.to.map((email) => encodeURIComponent(email)).join(",");
const useStartTLS = config.useTLS ? "yes" : "no";
- return `smtp://${auth}@${host}/?from=${encodeURIComponent(config.from)}&to=${toRecipients}&starttls=${useStartTLS}`;
+ return `smtp://${auth}${host}/?from=${encodeURIComponent(config.from)}&to=${toRecipients}&starttls=${useStartTLS}`;
}
diff --git a/app/server/modules/notifications/notifications.dto.ts b/app/server/modules/notifications/notifications.dto.ts
index 65d682f6..887a93ae 100644
--- a/app/server/modules/notifications/notifications.dto.ts
+++ b/app/server/modules/notifications/notifications.dto.ts
@@ -190,6 +190,7 @@ export const scheduleNotificationAssignmentSchema = type({
destinationId: "number",
notifyOnStart: "boolean",
notifyOnSuccess: "boolean",
+ notifyOnWarning: "boolean",
notifyOnFailure: "boolean",
createdAt: "number",
destination: notificationDestinationSchema,
@@ -227,6 +228,7 @@ export const updateScheduleNotificationsBody = type({
destinationId: "number",
notifyOnStart: "boolean",
notifyOnSuccess: "boolean",
+ notifyOnWarning: "boolean",
notifyOnFailure: "boolean",
}).array(),
});
diff --git a/app/server/modules/notifications/notifications.service.ts b/app/server/modules/notifications/notifications.service.ts
index 4099d8b4..b25b79f6 100644
--- a/app/server/modules/notifications/notifications.service.ts
+++ b/app/server/modules/notifications/notifications.service.ts
@@ -38,7 +38,7 @@ async function encryptSensitiveFields(config: NotificationConfig): Promise,
) => {
@@ -300,8 +301,9 @@ const sendBackupNotification = async (
return assignment.notifyOnStart;
case "success":
return assignment.notifyOnSuccess;
- case "failure":
case "warning":
+ return assignment.notifyOnWarning;
+ case "failure":
return assignment.notifyOnFailure;
default:
return false;
diff --git a/app/server/modules/repositories/repositories.controller.ts b/app/server/modules/repositories/repositories.controller.ts
index c629deca..a53a3a29 100644
--- a/app/server/modules/repositories/repositories.controller.ts
+++ b/app/server/modules/repositories/repositories.controller.ts
@@ -90,6 +90,7 @@ export const repositoriesController = new Hono()
short_id: snapshot.short_id,
duration,
paths: snapshot.paths,
+ tags: snapshot.tags ?? [],
size: summary?.total_bytes_processed || 0,
time: new Date(snapshot.time).getTime(),
};
@@ -113,6 +114,7 @@ export const repositoriesController = new Hono()
time: new Date(snapshot.time).getTime(),
paths: snapshot.paths,
size: snapshot.summary?.total_bytes_processed || 0,
+ tags: snapshot.tags ?? [],
summary: snapshot.summary,
};
diff --git a/app/server/modules/repositories/repositories.dto.ts b/app/server/modules/repositories/repositories.dto.ts
index aa77a62e..c659b18b 100644
--- a/app/server/modules/repositories/repositories.dto.ts
+++ b/app/server/modules/repositories/repositories.dto.ts
@@ -174,6 +174,7 @@ export const snapshotSchema = type({
paths: "string[]",
size: "number",
duration: "number",
+ tags: "string[]",
});
const listSnapshotsResponse = snapshotSchema.array();
diff --git a/app/server/modules/repositories/repositories.service.ts b/app/server/modules/repositories/repositories.service.ts
index 127a8275..ea23a161 100644
--- a/app/server/modules/repositories/repositories.service.ts
+++ b/app/server/modules/repositories/repositories.service.ts
@@ -8,6 +8,7 @@ import { toMessage } from "../../utils/errors";
import { generateShortId } from "../../utils/id";
import { restic } from "../../utils/restic";
import { cryptoUtils } from "../../utils/crypto";
+import { repoMutex } from "../../core/repository-mutex";
import type { CompressionMode, OverwriteMode, RepositoryConfig } from "~/schemas/restic";
const listRepositories = async () => {
@@ -160,15 +161,20 @@ const listSnapshots = async (name: string, backupId?: string) => {
throw new NotFoundError("Repository not found");
}
- let snapshots = [];
+ const releaseLock = await repoMutex.acquireShared(repository.id, "snapshots");
+ try {
+ let snapshots = [];
- if (backupId) {
- snapshots = await restic.snapshots(repository.config, { tags: [backupId.toString()] });
- } else {
- snapshots = await restic.snapshots(repository.config);
+ if (backupId) {
+ snapshots = await restic.snapshots(repository.config, { tags: [backupId.toString()] });
+ } else {
+ snapshots = await restic.snapshots(repository.config);
+ }
+
+ return snapshots;
+ } finally {
+ releaseLock();
}
-
- return snapshots;
};
const listSnapshotFiles = async (name: string, snapshotId: string, path?: string) => {
@@ -180,22 +186,27 @@ const listSnapshotFiles = async (name: string, snapshotId: string, path?: string
throw new NotFoundError("Repository not found");
}
- const result = await restic.ls(repository.config, snapshotId, path);
+ const releaseLock = await repoMutex.acquireShared(repository.id, `ls:${snapshotId}`);
+ try {
+ const result = await restic.ls(repository.config, snapshotId, path);
- if (!result.snapshot) {
- throw new NotFoundError("Snapshot not found or empty");
+ if (!result.snapshot) {
+ throw new NotFoundError("Snapshot not found or empty");
+ }
+
+ return {
+ snapshot: {
+ id: result.snapshot.id,
+ short_id: result.snapshot.short_id,
+ time: result.snapshot.time,
+ hostname: result.snapshot.hostname,
+ paths: result.snapshot.paths,
+ },
+ files: result.nodes,
+ };
+ } finally {
+ releaseLock();
}
-
- return {
- snapshot: {
- id: result.snapshot.id,
- short_id: result.snapshot.short_id,
- time: result.snapshot.time,
- hostname: result.snapshot.hostname,
- paths: result.snapshot.paths,
- },
- files: result.nodes,
- };
};
const restoreSnapshot = async (
@@ -220,14 +231,19 @@ const restoreSnapshot = async (
const target = options?.targetPath || "/";
- const result = await restic.restore(repository.config, snapshotId, target, options);
+ const releaseLock = await repoMutex.acquireShared(repository.id, `restore:${snapshotId}`);
+ try {
+ const result = await restic.restore(repository.config, snapshotId, target, options);
- return {
- success: true,
- message: "Snapshot restored successfully",
- filesRestored: result.files_restored,
- filesSkipped: result.files_skipped,
- };
+ return {
+ success: true,
+ message: "Snapshot restored successfully",
+ filesRestored: result.files_restored,
+ filesSkipped: result.files_skipped,
+ };
+ } finally {
+ releaseLock();
+ }
};
const getSnapshotDetails = async (name: string, snapshotId: string) => {
@@ -239,14 +255,19 @@ const getSnapshotDetails = async (name: string, snapshotId: string) => {
throw new NotFoundError("Repository not found");
}
- const snapshots = await restic.snapshots(repository.config);
- const snapshot = snapshots.find((snap) => snap.id === snapshotId || snap.short_id === snapshotId);
+ const releaseLock = await repoMutex.acquireShared(repository.id, `snapshot_details:${snapshotId}`);
+ try {
+ const snapshots = await restic.snapshots(repository.config);
+ const snapshot = snapshots.find((snap) => snap.id === snapshotId || snap.short_id === snapshotId);
- if (!snapshot) {
- throw new NotFoundError("Snapshot not found");
+ if (!snapshot) {
+ throw new NotFoundError("Snapshot not found");
+ }
+
+ return snapshot;
+ } finally {
+ releaseLock();
}
-
- return snapshot;
};
const checkHealth = async (repositoryId: string) => {
@@ -258,18 +279,23 @@ const checkHealth = async (repositoryId: string) => {
throw new NotFoundError("Repository not found");
}
- const { hasErrors, error } = await restic.check(repository.config);
+ const releaseLock = await repoMutex.acquireExclusive(repository.id, "check");
+ try {
+ const { hasErrors, error } = await restic.check(repository.config);
- await db
- .update(repositoriesTable)
- .set({
- status: hasErrors ? "error" : "healthy",
- lastChecked: Date.now(),
- lastError: error,
- })
- .where(eq(repositoriesTable.id, repository.id));
+ await db
+ .update(repositoriesTable)
+ .set({
+ status: hasErrors ? "error" : "healthy",
+ lastChecked: Date.now(),
+ lastError: error,
+ })
+ .where(eq(repositoriesTable.id, repository.id));
- return { lastError: error };
+ return { lastError: error };
+ } finally {
+ releaseLock();
+ }
};
const doctorRepository = async (name: string) => {
@@ -295,48 +321,51 @@ const doctorRepository = async (name: string) => {
error: unlockResult.error,
});
- const checkResult = await restic.check(repository.config, { readData: false }).then(
- (result) => result,
- (error) => ({ success: false, output: null, error: toMessage(error), hasErrors: true }),
- );
-
- steps.push({
- step: "check",
- success: checkResult.success,
- output: checkResult.output,
- error: checkResult.error,
- });
-
- if (checkResult.hasErrors) {
- const repairResult = await restic.repairIndex(repository.config).then(
- (result) => ({ success: true, output: result.output, error: null }),
- (error) => ({ success: false, output: null, error: toMessage(error) }),
- );
-
- steps.push({
- step: "repair_index",
- success: repairResult.success,
- output: repairResult.output,
- error: repairResult.error,
- });
-
- const recheckResult = await restic.check(repository.config, { readData: false }).then(
+ const releaseLock = await repoMutex.acquireExclusive(repository.id, "doctor");
+ try {
+ const checkResult = await restic.check(repository.config, { readData: false }).then(
(result) => result,
(error) => ({ success: false, output: null, error: toMessage(error), hasErrors: true }),
);
steps.push({
- step: "recheck",
- success: recheckResult.success,
- output: recheckResult.output,
- error: recheckResult.error,
+ step: "check",
+ success: checkResult.success,
+ output: checkResult.output,
+ error: checkResult.error,
});
+
+ if (checkResult.hasErrors) {
+ const repairResult = await restic.repairIndex(repository.config).then(
+ (result) => ({ success: true, output: result.output, error: null }),
+ (error) => ({ success: false, output: null, error: toMessage(error) }),
+ );
+
+ steps.push({
+ step: "repair_index",
+ success: repairResult.success,
+ output: repairResult.output,
+ error: repairResult.error,
+ });
+
+ const recheckResult = await restic.check(repository.config, { readData: false }).then(
+ (result) => result,
+ (error) => ({ success: false, output: null, error: toMessage(error), hasErrors: true }),
+ );
+
+ steps.push({
+ step: "recheck",
+ success: recheckResult.success,
+ output: recheckResult.output,
+ error: recheckResult.error,
+ });
+ }
+ } finally {
+ releaseLock();
}
const allSuccessful = steps.every((s) => s.success);
- console.log("Doctor steps:", steps);
-
await db
.update(repositoriesTable)
.set({
@@ -361,7 +390,12 @@ const deleteSnapshot = async (name: string, snapshotId: string) => {
throw new NotFoundError("Repository not found");
}
- await restic.deleteSnapshot(repository.config, snapshotId);
+ const releaseLock = await repoMutex.acquireExclusive(repository.id, `delete:${snapshotId}`);
+ try {
+ await restic.deleteSnapshot(repository.config, snapshotId);
+ } finally {
+ releaseLock();
+ }
};
const updateRepository = async (name: string, updates: { name?: string; compressionMode?: CompressionMode }) => {
diff --git a/app/server/modules/volumes/volume.service.ts b/app/server/modules/volumes/volume.service.ts
index 7169d6fc..3f415c0b 100644
--- a/app/server/modules/volumes/volume.service.ts
+++ b/app/server/modules/volumes/volume.service.ts
@@ -5,9 +5,10 @@ import Docker from "dockerode";
import { and, eq, ne } from "drizzle-orm";
import { ConflictError, InternalServerError, NotFoundError } from "http-errors-enhanced";
import slugify from "slugify";
-import { getCapabilities } from "../../core/capabilities";
+import { getCapabilities, parseDockerHost } from "../../core/capabilities";
import { db } from "../../db/db";
import { volumesTable } from "../../db/schema";
+import { cryptoUtils } from "../../utils/crypto";
import { toMessage } from "../../utils/errors";
import { generateShortId } from "../../utils/id";
import { getStatFs, type StatFs } from "../../utils/mountinfo";
@@ -19,6 +20,23 @@ import { logger } from "../../utils/logger";
import { serverEvents } from "../../core/events";
import type { BackendConfig } from "~/schemas/volumes";
+async function encryptSensitiveFields(config: BackendConfig): Promise {
+ switch (config.backend) {
+ case "smb":
+ return {
+ ...config,
+ password: await cryptoUtils.encrypt(config.password),
+ };
+ case "webdav":
+ return {
+ ...config,
+ password: config.password ? await cryptoUtils.encrypt(config.password) : undefined,
+ };
+ default:
+ return config;
+ }
+}
+
const listVolumes = async () => {
const volumes = await db.query.volumesTable.findMany({});
@@ -37,13 +55,14 @@ const createVolume = async (name: string, backendConfig: BackendConfig) => {
}
const shortId = generateShortId();
+ const encryptedConfig = await encryptSensitiveFields(backendConfig);
const [created] = await db
.insert(volumesTable)
.values({
shortId,
name: slug,
- config: backendConfig,
+ config: encryptedConfig,
type: backendConfig.backend,
})
.returning();
@@ -175,11 +194,13 @@ const updateVolume = async (name: string, volumeData: UpdateVolumeBody) => {
await backend.unmount();
}
+ const encryptedConfig = volumeData.config ? await encryptSensitiveFields(volumeData.config) : undefined;
+
const [updated] = await db
.update(volumesTable)
.set({
name: newName,
- config: volumeData.config,
+ config: encryptedConfig,
type: volumeData.config?.backend,
autoRemount: volumeData.autoRemount,
updatedAt: Date.now(),
@@ -277,7 +298,8 @@ const getContainersUsingVolume = async (name: string) => {
}
try {
- const docker = new Docker();
+ const docker = new Docker(parseDockerHost(process.env.DOCKER_HOST));
+
const containers = await docker.listContainers({ all: true });
const usingContainers = [];
diff --git a/app/server/utils/backend-compatibility.ts b/app/server/utils/backend-compatibility.ts
new file mode 100644
index 00000000..329db412
--- /dev/null
+++ b/app/server/utils/backend-compatibility.ts
@@ -0,0 +1,148 @@
+import type { RepositoryConfig } from "~/schemas/restic";
+import { cryptoUtils } from "./crypto";
+
+type BackendConflictGroup = "s3" | "gcs" | "azure" | "rest" | "sftp" | null;
+
+export const getBackendConflictGroup = (backend: string): BackendConflictGroup => {
+ switch (backend) {
+ case "s3":
+ case "r2":
+ return "s3";
+ case "gcs":
+ return "gcs";
+ case "azure":
+ return "azure";
+ case "rest":
+ return "rest";
+ case "sftp":
+ return "sftp";
+ case "local":
+ case "rclone":
+ return null;
+ default:
+ return null;
+ }
+};
+
+export const hasCompatibleCredentials = async (
+ config1: RepositoryConfig,
+ config2: RepositoryConfig,
+): Promise => {
+ const group1 = getBackendConflictGroup(config1.backend);
+ const group2 = getBackendConflictGroup(config2.backend);
+
+ if (!group1 || !group2 || group1 !== group2) {
+ return true;
+ }
+
+ switch (group1) {
+ case "s3": {
+ if (
+ (config1.backend === "s3" || config1.backend === "r2") &&
+ (config2.backend === "s3" || config2.backend === "r2")
+ ) {
+ const accessKey1 = await cryptoUtils.decrypt(config1.accessKeyId);
+ const secretKey1 = await cryptoUtils.decrypt(config1.secretAccessKey);
+
+ const accessKey2 = await cryptoUtils.decrypt(config2.accessKeyId);
+ const secretKey2 = await cryptoUtils.decrypt(config2.secretAccessKey);
+
+ return accessKey1 === accessKey2 && secretKey1 === secretKey2;
+ }
+ return false;
+ }
+ case "gcs": {
+ if (config1.backend === "gcs" && config2.backend === "gcs") {
+ const credentials1 = await cryptoUtils.decrypt(config1.credentialsJson);
+ const credentials2 = await cryptoUtils.decrypt(config2.credentialsJson);
+
+ return credentials1 === credentials2 && config1.projectId === config2.projectId;
+ }
+ return false;
+ }
+ case "azure": {
+ if (config1.backend === "azure" && config2.backend === "azure") {
+ const config1Accountkey = await cryptoUtils.decrypt(config1.accountKey);
+ const config2Accountkey = await cryptoUtils.decrypt(config2.accountKey);
+
+ return config1.accountName === config2.accountName && config1Accountkey === config2Accountkey;
+ }
+ return false;
+ }
+ case "rest": {
+ if (config1.backend === "rest" && config2.backend === "rest") {
+ if (!config1.username && !config2.username && !config1.password && !config2.password) {
+ return true;
+ }
+
+ const config1Username = await cryptoUtils.decrypt(config1.username || "");
+ const config1Password = await cryptoUtils.decrypt(config1.password || "");
+ const config2Username = await cryptoUtils.decrypt(config2.username || "");
+ const config2Password = await cryptoUtils.decrypt(config2.password || "");
+
+ return config1Username === config2Username && config1Password === config2Password;
+ }
+ return false;
+ }
+ case "sftp": {
+ return false;
+ }
+ default:
+ return false;
+ }
+};
+
+export interface CompatibilityResult {
+ repositoryId: string;
+ compatible: boolean;
+ reason: string | null;
+}
+
+export const checkMirrorCompatibility = async (
+ primaryConfig: RepositoryConfig,
+ mirrorConfig: RepositoryConfig,
+ mirrorRepositoryId: string,
+): Promise => {
+ const primaryConflictGroup = getBackendConflictGroup(primaryConfig.backend);
+ const mirrorConflictGroup = getBackendConflictGroup(mirrorConfig.backend);
+
+ if (!primaryConflictGroup || !mirrorConflictGroup) {
+ return {
+ repositoryId: mirrorRepositoryId,
+ compatible: true,
+ reason: null,
+ };
+ }
+
+ if (primaryConflictGroup !== mirrorConflictGroup) {
+ return {
+ repositoryId: mirrorRepositoryId,
+ compatible: true,
+ reason: null,
+ };
+ }
+
+ const compatible = await hasCompatibleCredentials(primaryConfig, mirrorConfig);
+
+ if (compatible) {
+ return {
+ repositoryId: mirrorRepositoryId,
+ compatible: true,
+ reason: null,
+ };
+ }
+
+ return {
+ repositoryId: mirrorRepositoryId,
+ compatible: false,
+ reason: `Both use ${primaryConflictGroup.toUpperCase()} backends with different credentials`,
+ };
+};
+
+export const getIncompatibleMirrorError = (mirrorRepoName: string, primaryBackend: string, mirrorBackend: string) => {
+ return (
+ `Cannot mirror to ${mirrorRepoName}: both repositories use the same backend type (${primaryBackend}/${mirrorBackend}) with different credentials. ` +
+ "Restic cannot use different credentials for the same backend in a copy operation. " +
+ "Consider creating a new backup scheduler with the desired destination instead."
+ );
+};
diff --git a/app/server/utils/crypto.ts b/app/server/utils/crypto.ts
index 5394cefd..c096f558 100644
--- a/app/server/utils/crypto.ts
+++ b/app/server/utils/crypto.ts
@@ -10,7 +10,8 @@ const isEncrypted = (val?: string): boolean => {
};
/**
- * Given a string, encrypts it using a randomly generated salt
+ * Given a string, encrypts it using a randomly generated salt.
+ * Returns the input unchanged if it's empty or already encrypted.
*/
const encrypt = async (data: string) => {
if (!data) {
@@ -21,7 +22,7 @@ const encrypt = async (data: string) => {
return data;
}
- const secret = (await Bun.file(RESTIC_PASS_FILE).text()).trim();
+ const secret = await Bun.file(RESTIC_PASS_FILE).text();
const salt = crypto.randomBytes(16);
const key = crypto.pbkdf2Sync(secret, salt, 100000, keyLength, "sha256");
@@ -35,14 +36,15 @@ const encrypt = async (data: string) => {
};
/**
- * Given an encrypted string, decrypts it using the salt stored in the string
+ * Given an encrypted string, decrypts it using the salt stored in the string.
+ * Returns the input unchanged if it's not encrypted (for backward compatibility).
*/
const decrypt = async (encryptedData: string) => {
if (!isEncrypted(encryptedData)) {
return encryptedData;
}
- const secret = await Bun.file(RESTIC_PASS_FILE).text();
+ const secret = (await Bun.file(RESTIC_PASS_FILE).text()).trim();
const parts = encryptedData.split(":").slice(1); // Remove prefix
const saltHex = parts.shift() as string;
diff --git a/app/server/utils/logger.ts b/app/server/utils/logger.ts
index c5b77adb..82eac353 100644
--- a/app/server/utils/logger.ts
+++ b/app/server/utils/logger.ts
@@ -1,15 +1,17 @@
import { createLogger, format, transports } from "winston";
import { sanitizeSensitiveData } from "./sanitize";
+import { config } from "../core/config";
const { printf, combine, colorize } = format;
const printConsole = printf((info) => `${info.level} > ${info.message}`);
const consoleFormat = combine(colorize(), printConsole);
+const defaultLevel = config.__prod__ ? "info" : "debug";
const winstonLogger = createLogger({
- level: "debug",
+ level: process.env.LOG_LEVEL || defaultLevel,
format: format.json(),
- transports: [new transports.Console({ level: "debug", format: consoleFormat })],
+ transports: [new transports.Console({ level: process.env.LOG_LEVEL || defaultLevel, format: consoleFormat })],
});
const log = (level: "info" | "warn" | "error" | "debug", messages: unknown[]) => {
diff --git a/app/server/utils/restic.ts b/app/server/utils/restic.ts
index 28e01cee..85d70d42 100644
--- a/app/server/utils/restic.ts
+++ b/app/server/utils/restic.ts
@@ -1,6 +1,7 @@
import crypto from "node:crypto";
import fs from "node:fs/promises";
import path from "node:path";
+import os from "node:os";
import { throttle } from "es-toolkit";
import { type } from "arktype";
import { $ } from "bun";
@@ -40,6 +41,7 @@ const snapshotInfoSchema = type({
time: "string",
uid: "number?",
username: "string",
+ tags: "string[]?",
summary: type({
backup_end: "string",
backup_start: "string",
@@ -200,8 +202,8 @@ const init = async (config: RepositoryConfig) => {
const env = await buildEnv(config);
- const args = ["init", "--repo", repoUrl, "--json"];
- addRepoSpecificArgs(args, config, env);
+ const args = ["init", "--repo", repoUrl];
+ addCommonArgs(args, env);
const res = await $`restic ${args}`.env(env).nothrow();
await cleanupTemporaryKeys(config, env);
@@ -233,6 +235,7 @@ const backup = async (
source: string,
options?: {
exclude?: string[];
+ excludeIfPresent?: string[];
include?: string[];
tags?: string[];
compressionMode?: CompressionMode;
@@ -260,8 +263,9 @@ const backup = async (
let includeFile: string | null = null;
if (options?.include && options.include.length > 0) {
- const tmp = await fs.mkdtemp("restic-include");
+ const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "zerobyte-restic-include-"));
includeFile = path.join(tmp, `include.txt`);
+
const includePaths = options.include.map((p) => path.join(source, p));
await fs.writeFile(includeFile, includePaths.join("\n"), "utf-8");
@@ -277,8 +281,13 @@ const backup = async (
}
}
- addRepoSpecificArgs(args, config, env);
- args.push("--json");
+ if (options?.excludeIfPresent && options.excludeIfPresent.length > 0) {
+ for (const filename of options.excludeIfPresent) {
+ args.push("--exclude-if-present", filename);
+ }
+ }
+
+ addCommonArgs(args, env);
const logData = throttle((data: string) => {
logger.info(data.trim());
@@ -404,8 +413,7 @@ const restore = async (
}
}
- addRepoSpecificArgs(args, config, env);
- args.push("--json");
+ addCommonArgs(args, env);
logger.debug(`Executing: restic ${args.join(" ")}`);
const res = await $`restic ${args}`.env(env).nothrow();
@@ -468,8 +476,7 @@ const snapshots = async (config: RepositoryConfig, options: { tags?: string[] }
}
}
- addRepoSpecificArgs(args, config, env);
- args.push("--json");
+ addCommonArgs(args, env);
const res = await $`restic ${args}`.env(env).nothrow().quiet();
await cleanupTemporaryKeys(config, env);
@@ -518,8 +525,7 @@ const forget = async (config: RepositoryConfig, options: RetentionPolicy, extra:
}
args.push("--prune");
- addRepoSpecificArgs(args, config, env);
- args.push("--json");
+ addCommonArgs(args, env);
const res = await $`restic ${args}`.env(env).nothrow();
await cleanupTemporaryKeys(config, env);
@@ -537,7 +543,7 @@ const deleteSnapshot = async (config: RepositoryConfig, snapshotId: string) => {
const env = await buildEnv(config);
const args: string[] = ["--repo", repoUrl, "forget", snapshotId, "--prune"];
- addRepoSpecificArgs(args, config, env);
+ addCommonArgs(args, env);
const res = await $`restic ${args}`.env(env).nothrow();
await cleanupTemporaryKeys(config, env);
@@ -581,13 +587,13 @@ const ls = async (config: RepositoryConfig, snapshotId: string, path?: string) =
const repoUrl = buildRepoUrl(config);
const env = await buildEnv(config);
- const args: string[] = ["--repo", repoUrl, "ls", snapshotId, "--json", "--long"];
+ const args: string[] = ["--repo", repoUrl, "ls", snapshotId, "--long"];
if (path) {
args.push(path);
}
- addRepoSpecificArgs(args, config, env);
+ addCommonArgs(args, env);
const res = await safeSpawn({ command: "restic", args, env });
await cleanupTemporaryKeys(config, env);
@@ -637,8 +643,8 @@ const unlock = async (config: RepositoryConfig) => {
const repoUrl = buildRepoUrl(config);
const env = await buildEnv(config);
- const args = ["unlock", "--repo", repoUrl, "--remove-all", "--json"];
- addRepoSpecificArgs(args, config, env);
+ const args = ["unlock", "--repo", repoUrl, "--remove-all"];
+ addCommonArgs(args, env);
const res = await $`restic ${args}`.env(env).nothrow();
await cleanupTemporaryKeys(config, env);
@@ -662,7 +668,7 @@ const check = async (config: RepositoryConfig, options?: { readData?: boolean })
args.push("--read-data");
}
- addRepoSpecificArgs(args, config, env);
+ addCommonArgs(args, env);
const res = await $`restic ${args}`.env(env).nothrow();
await cleanupTemporaryKeys(config, env);
@@ -696,7 +702,7 @@ const repairIndex = async (config: RepositoryConfig) => {
const env = await buildEnv(config);
const args = ["repair", "index", "--repo", repoUrl];
- addRepoSpecificArgs(args, config, env);
+ addCommonArgs(args, env);
const res = await $`restic ${args}`.env(env).nothrow();
await cleanupTemporaryKeys(config, env);
@@ -717,10 +723,65 @@ const repairIndex = async (config: RepositoryConfig) => {
};
};
-const addRepoSpecificArgs = (args: string[], config: RepositoryConfig, env: Record) => {
- if (config.backend === "sftp" && env._SFTP_SSH_ARGS) {
- args.push("-o", `sftp.args=${env._SFTP_SSH_ARGS}`);
+const copy = async (
+ sourceConfig: RepositoryConfig,
+ destConfig: RepositoryConfig,
+ options: {
+ tag?: string;
+ snapshotId?: string;
+ },
+) => {
+ const sourceRepoUrl = buildRepoUrl(sourceConfig);
+ const destRepoUrl = buildRepoUrl(destConfig);
+
+ const sourceEnv = await buildEnv(sourceConfig);
+ const destEnv = await buildEnv(destConfig);
+
+ const env: Record = {
+ ...sourceEnv,
+ ...destEnv,
+ RESTIC_FROM_PASSWORD_FILE: sourceEnv.RESTIC_PASSWORD_FILE,
+ };
+
+ const args: string[] = ["--repo", destRepoUrl, "copy", "--from-repo", sourceRepoUrl];
+
+ if (options.tag) {
+ args.push("--tag", options.tag);
}
+
+ if (options.snapshotId) {
+ args.push(options.snapshotId);
+ } else {
+ args.push("latest");
+ }
+
+ addCommonArgs(args, env);
+
+ if (sourceConfig.backend === "sftp" && sourceEnv._SFTP_SSH_ARGS) {
+ args.push("-o", `sftp.args=${sourceEnv._SFTP_SSH_ARGS}`);
+ }
+
+ logger.info(`Copying snapshots from ${sourceRepoUrl} to ${destRepoUrl}...`);
+ logger.debug(`Executing: restic ${args.join(" ")}`);
+
+ const res = await $`restic ${args}`.env(env).nothrow();
+
+ await cleanupTemporaryKeys(sourceConfig, sourceEnv);
+ await cleanupTemporaryKeys(destConfig, destEnv);
+
+ const stdout = res.text();
+ const stderr = res.stderr.toString();
+
+ if (res.exitCode !== 0) {
+ logger.error(`Restic copy failed: ${stderr}`);
+ throw new ResticError(res.exitCode, stderr);
+ }
+
+ logger.info(`Restic copy completed from ${sourceRepoUrl} to ${destRepoUrl}`);
+ return {
+ success: true,
+ output: stdout,
+ };
};
const cleanupTemporaryKeys = async (config: RepositoryConfig, env: Record) => {
@@ -733,6 +794,14 @@ const cleanupTemporaryKeys = async (config: RepositoryConfig, env: Record) => {
+ args.push("--json");
+
+ if (env._SFTP_SSH_ARGS) {
+ args.push("-o", `sftp.args=${env._SFTP_SSH_ARGS}`);
+ }
+};
+
export const restic = {
ensurePassfile,
init,
@@ -745,4 +814,5 @@ export const restic = {
ls,
check,
repairIndex,
+ copy,
};
diff --git a/biome.json b/biome.json
index 68a6cf18..928a49a7 100644
--- a/biome.json
+++ b/biome.json
@@ -1,8 +1,9 @@
{
- "$schema": "https://biomejs.dev/schemas/2.3.5/schema.json",
+ "$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
+ "defaultBranch": "origin/main",
"useIgnoreFile": true
},
"files": {
diff --git a/bun.lock b/bun.lock
index 489487d6..66bf1d8d 100644
--- a/bun.lock
+++ b/bun.lock
@@ -1,27 +1,28 @@
{
"lockfileVersion": 1,
+ "configVersion": 0,
"workspaces": {
"": {
"dependencies": {
- "@hono/standard-validator": "^0.1.5",
+ "@hono/standard-validator": "^0.2.0",
"@hookform/resolvers": "^5.2.2",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.15",
- "@radix-ui/react-label": "^2.1.7",
+ "@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-progress": "^1.1.8",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.2.6",
- "@radix-ui/react-separator": "^1.1.7",
- "@radix-ui/react-slot": "^1.2.3",
+ "@radix-ui/react-separator": "^1.1.8",
+ "@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.8",
- "@react-router/node": "^7.9.3",
- "@react-router/serve": "^7.9.3",
- "@scalar/hono-api-reference": "^0.9.24",
- "@tanstack/react-query": "^5.90.2",
- "arktype": "^2.1.26",
+ "@react-router/node": "^7.10.0",
+ "@react-router/serve": "^7.10.0",
+ "@scalar/hono-api-reference": "^0.9.25",
+ "@tanstack/react-query": "^5.90.11",
+ "arktype": "^2.1.28",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cron-parser": "^5.4.0",
@@ -30,54 +31,54 @@
"dockerode": "^4.0.9",
"dotenv": "^17.2.3",
"drizzle-orm": "^0.44.7",
- "es-toolkit": "^1.41.0",
- "hono": "^4.10.5",
+ "es-toolkit": "^1.42.0",
+ "hono": "4.10.5",
"hono-openapi": "^1.1.1",
- "http-errors-enhanced": "^3.0.2",
- "isbot": "^5.1.31",
- "lucide-react": "^0.546.0",
+ "http-errors-enhanced": "^4.0.2",
+ "isbot": "^5.1.32",
+ "lucide-react": "^0.555.0",
"next-themes": "^0.4.6",
"node-cron": "^4.2.1",
- "react": "^19.2.0",
- "react-dom": "^19.2.0",
- "react-hook-form": "^7.63.0",
- "react-router": "^7.9.3",
+ "react": "^19.2.1",
+ "react-dom": "^19.2.1",
+ "react-hook-form": "^7.68.0",
+ "react-router": "^7.10.0",
"react-router-hono-server": "^2.22.0",
- "recharts": "3.2.1",
+ "recharts": "3.5.1",
"slugify": "^1.6.6",
"sonner": "^2.0.7",
- "tailwind-merge": "^3.3.1",
+ "tailwind-merge": "^3.4.0",
"tiny-typed-emitter": "^2.1.0",
"winston": "^3.18.3",
- "yaml": "^2.8.1",
+ "yaml": "^2.8.2",
},
"devDependencies": {
- "@biomejs/biome": "^2.3.5",
- "@hey-api/openapi-ts": "^0.87.4",
- "@react-router/dev": "^7.9.3",
- "@tailwindcss/vite": "^4.1.14",
- "@tanstack/react-query-devtools": "^5.90.2",
- "@types/bun": "^1.3.2",
- "@types/dockerode": "^3.3.45",
- "@types/node": "^24.6.2",
- "@types/react": "^19.2.0",
- "@types/react-dom": "^19.2.0",
- "drizzle-kit": "^0.31.6",
+ "@biomejs/biome": "^2.3.8",
+ "@hey-api/openapi-ts": "^0.88.0",
+ "@react-router/dev": "^7.10.0",
+ "@tailwindcss/vite": "^4.1.17",
+ "@tanstack/react-query-devtools": "^5.91.1",
+ "@types/bun": "^1.3.3",
+ "@types/dockerode": "^3.3.47",
+ "@types/node": "^24.10.1",
+ "@types/react": "^19.2.7",
+ "@types/react-dom": "^19.2.3",
+ "drizzle-kit": "^0.31.7",
"lightningcss": "^1.30.2",
- "tailwindcss": "^4.1.14",
+ "tailwindcss": "^4.1.17",
"tinyglobby": "^0.2.15",
"tw-animate-css": "^1.4.0",
"typescript": "^5.9.3",
- "vite": "^7.1.9",
+ "vite": "^7.2.6",
"vite-bundle-analyzer": "^1.2.3",
"vite-tsconfig-paths": "^5.1.4",
},
},
},
"packages": {
- "@ark/schema": ["@ark/schema@0.54.0", "", { "dependencies": { "@ark/util": "0.54.0" } }, "sha512-QloFou+ODfb5qXgPxX1EbJyRqZEwoElzvJ6VuuFVvWJQGoigBEUW3L0HejXG/B9v8K3VvDikuULp5ELSwZn8hg=="],
+ "@ark/schema": ["@ark/schema@0.56.0", "", { "dependencies": { "@ark/util": "0.56.0" } }, "sha512-ECg3hox/6Z/nLajxXqNhgPtNdHWC9zNsDyskwO28WinoFEnWow4IsERNz9AnXRhTZJnYIlAJ4uGn3nlLk65vZA=="],
- "@ark/util": ["@ark/util@0.54.0", "", {}, "sha512-ugTfpEDGA6d2uU2/3M7uRiuPNYckQMhg2wfNMw8zZHXjPhuVCbXWZzIcBojuXuCN8j4MrNWNqQ9z7f8W/JiE8w=="],
+ "@ark/util": ["@ark/util@0.56.0", "", {}, "sha512-BghfRC8b9pNs3vBoDJhcta0/c1J1rsoS1+HgVUreMFPdhz/CRAKReAu57YEllNaSy98rWAdY1gE+gFup7OXpgA=="],
"@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
@@ -137,23 +138,23 @@
"@balena/dockerignore": ["@balena/dockerignore@1.0.2", "", {}, "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q=="],
- "@biomejs/biome": ["@biomejs/biome@2.3.5", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.5", "@biomejs/cli-darwin-x64": "2.3.5", "@biomejs/cli-linux-arm64": "2.3.5", "@biomejs/cli-linux-arm64-musl": "2.3.5", "@biomejs/cli-linux-x64": "2.3.5", "@biomejs/cli-linux-x64-musl": "2.3.5", "@biomejs/cli-win32-arm64": "2.3.5", "@biomejs/cli-win32-x64": "2.3.5" }, "bin": { "biome": "bin/biome" } }, "sha512-HvLhNlIlBIbAV77VysRIBEwp55oM/QAjQEin74QQX9Xb259/XP/D5AGGnZMOyF1el4zcvlNYYR3AyTMUV3ILhg=="],
+ "@biomejs/biome": ["@biomejs/biome@2.3.8", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.8", "@biomejs/cli-darwin-x64": "2.3.8", "@biomejs/cli-linux-arm64": "2.3.8", "@biomejs/cli-linux-arm64-musl": "2.3.8", "@biomejs/cli-linux-x64": "2.3.8", "@biomejs/cli-linux-x64-musl": "2.3.8", "@biomejs/cli-win32-arm64": "2.3.8", "@biomejs/cli-win32-x64": "2.3.8" }, "bin": { "biome": "bin/biome" } }, "sha512-Qjsgoe6FEBxWAUzwFGFrB+1+M8y/y5kwmg5CHac+GSVOdmOIqsAiXM5QMVGZJ1eCUCLlPZtq4aFAQ0eawEUuUA=="],
- "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-fLdTur8cJU33HxHUUsii3GLx/TR0BsfQx8FkeqIiW33cGMtUD56fAtrh+2Fx1uhiCsVZlFh6iLKUU3pniZREQw=="],
+ "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-HM4Zg9CGQ3txTPflxD19n8MFPrmUAjaC7PQdLkugeeC0cQ+PiVrd7i09gaBS/11QKsTDBJhVg85CEIK9f50Qww=="],
- "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.3.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-qpT8XDqeUlzrOW8zb4k3tjhT7rmvVRumhi2657I2aGcY4B+Ft5fNwDdZGACzn8zj7/K1fdWjgwYE3i2mSZ+vOA=="],
+ "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.3.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-lUDQ03D7y/qEao7RgdjWVGCu+BLYadhKTm40HkpJIi6kn8LSv5PAwRlew/DmwP4YZ9ke9XXoTIQDO1vAnbRZlA=="],
- "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.3.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-u/pybjTBPGBHB66ku4pK1gj+Dxgx7/+Z0jAriZISPX1ocTO8aHh8x8e7Kb1rB4Ms0nA/SzjtNOVJ4exVavQBCw=="],
+ "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.3.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-Uo1OJnIkJgSgF+USx970fsM/drtPcQ39I+JO+Fjsaa9ZdCN1oysQmy6oAGbyESlouz+rzEckLTF6DS7cWse95g=="],
- "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.3.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-eGUG7+hcLgGnMNl1KHVZUYxahYAhC462jF/wQolqu4qso2MSk32Q+QrpN7eN4jAHAg7FUMIo897muIhK4hXhqg=="],
+ "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.3.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-PShR4mM0sjksUMyxbyPNMxoKFPVF48fU8Qe8Sfx6w6F42verbwRLbz+QiKNiDPRJwUoMG1nPM50OBL3aOnTevA=="],
- "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.3.5", "", { "os": "linux", "cpu": "x64" }, "sha512-XrIVi9YAW6ye0CGQ+yax0gLfx+BFOtKaNX74n+xHWla6Cl6huUmcKNO7HPx7BiKnJUzrxXY1qYlm7xMvi08X4g=="],
+ "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.3.8", "", { "os": "linux", "cpu": "x64" }, "sha512-QDPMD5bQz6qOVb3kiBui0zKZXASLo0NIQ9JVJio5RveBEFgDgsvJFUvZIbMbUZT3T00M/1wdzwWXk4GIh0KaAw=="],
- "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.3.5", "", { "os": "linux", "cpu": "x64" }, "sha512-awVuycTPpVTH/+WDVnEEYSf6nbCBHf/4wB3lquwT7puhNg8R4XvonWNZzUsfHZrCkjkLhFH/vCZK5jHatD9FEg=="],
+ "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.3.8", "", { "os": "linux", "cpu": "x64" }, "sha512-YGLkqU91r1276uwSjiUD/xaVikdxgV1QpsicT0bIA1TaieM6E5ibMZeSyjQ/izBn4tKQthUSsVZacmoJfa3pDA=="],
- "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.3.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-DlBiMlBZZ9eIq4H7RimDSGsYcOtfOIfZOaI5CqsWiSlbTfqbPVfWtCf92wNzx8GNMbu1s7/g3ZZESr6+GwM/SA=="],
+ "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.3.8", "", { "os": "win32", "cpu": "arm64" }, "sha512-H4IoCHvL1fXKDrTALeTKMiE7GGWFAraDwBYFquE/L/5r1927Te0mYIGseXi4F+lrrwhSWbSGt5qPFswNoBaCxg=="],
- "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.5", "", { "os": "win32", "cpu": "x64" }, "sha512-nUmR8gb6yvrKhtRgzwo/gDimPwnO5a4sCydf8ZS2kHIJhEmSmk+STsusr1LHTuM//wXppBawvSQi2xFXJCdgKQ=="],
+ "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.8", "", { "os": "win32", "cpu": "x64" }, "sha512-RguzimPoZWtBapfKhKjcWXBVI91tiSprqdBYu7tWhgN8pKRZhw24rFeNZTNf6UiBfjCYCi9eFQs/JzJZIhuK4w=="],
"@colors/colors": ["@colors/colors@1.6.0", "", {}, "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA=="],
@@ -233,20 +234,18 @@
"@hey-api/json-schema-ref-parser": ["@hey-api/json-schema-ref-parser@1.2.1", "", { "dependencies": { "@jsdevtools/ono": "^7.1.3", "@types/json-schema": "^7.0.15", "js-yaml": "^4.1.0", "lodash": "^4.17.21" } }, "sha512-inPeksRLq+j3ArnuGOzQPQE//YrhezQG0+9Y9yizScBN2qatJ78fIByhEgKdNAbtguDCn4RPxmEhcrePwHxs4A=="],
- "@hey-api/openapi-ts": ["@hey-api/openapi-ts@0.87.4", "", { "dependencies": { "@hey-api/codegen-core": "^0.3.3", "@hey-api/json-schema-ref-parser": "1.2.1", "ansi-colors": "4.1.3", "c12": "3.3.2", "color-support": "1.1.3", "commander": "14.0.1", "open": "10.2.0", "semver": "7.7.2" }, "peerDependencies": { "typescript": ">=5.5.3" }, "bin": { "openapi-ts": "bin/run.js" } }, "sha512-ogSjR7l+PCBoRvvJVrUFZ3Cv4GDnETpwAr5G9EwsbjKVJHZDsQn3EIOYJi2WqKl9VPZBT1CtycIxR/ZmCS/T4A=="],
+ "@hey-api/openapi-ts": ["@hey-api/openapi-ts@0.88.0", "", { "dependencies": { "@hey-api/codegen-core": "^0.3.3", "@hey-api/json-schema-ref-parser": "1.2.1", "ansi-colors": "4.1.3", "c12": "3.3.2", "color-support": "1.1.3", "commander": "14.0.2", "open": "11.0.0", "semver": "7.7.2" }, "peerDependencies": { "typescript": ">=5.5.3" }, "bin": { "openapi-ts": "bin/run.js" } }, "sha512-ZrvmDfmVf+N4ry786LAhS/DoH+xkIjIJIeDnj2aL1qnMTIDsdRIXXvr80EnAZVBgunzu1wTBbHb3H9OfyDQ2EQ=="],
"@hono/node-server": ["@hono/node-server@1.19.6", "", { "peerDependencies": { "hono": "^4" } }, "sha512-Shz/KjlIeAhfiuE93NDKVdZ7HdBVLQAfdbaXEaoAVO3ic9ibRSLGIQGkcBbFyuLr+7/1D5ZCINM8B+6IvXeMtw=="],
"@hono/node-ws": ["@hono/node-ws@1.2.0", "", { "dependencies": { "ws": "^8.17.0" }, "peerDependencies": { "@hono/node-server": "^1.11.1", "hono": "^4.6.0" } }, "sha512-OBPQ8OSHBw29mj00wT/xGYtB6HY54j0fNSdVZ7gZM3TUeq0So11GXaWtFf1xWxQNfumKIsj0wRuLKWfVsO5GgQ=="],
- "@hono/standard-validator": ["@hono/standard-validator@0.1.5", "", { "peerDependencies": { "@standard-schema/spec": "1.0.0", "hono": ">=3.9.0" } }, "sha512-EIyZPPwkyLn6XKwFj5NBEWHXhXbgmnVh2ceIFo5GO7gKI9WmzTjPDKnppQB0KrqKeAkq3kpoW4SIbu5X1dgx3w=="],
+ "@hono/standard-validator": ["@hono/standard-validator@0.2.0", "", { "peerDependencies": { "@standard-schema/spec": "1.0.0", "hono": ">=3.9.0" } }, "sha512-pFq0UVAnjzXcDAgqFpDeVL3MOUPrlIh/kPqBDvbCYoThVhhS+Vf37VcdsakdOFFGiqoiYVxp3LifXFhGhp/rgQ=="],
"@hono/vite-dev-server": ["@hono/vite-dev-server@0.23.0", "", { "dependencies": { "@hono/node-server": "^1.14.2", "minimatch": "^9.0.3" }, "peerDependencies": { "hono": "*", "miniflare": "*", "wrangler": "*" }, "optionalPeers": ["miniflare", "wrangler"] }, "sha512-tHV86xToed9Up0j/dubQW2PDP4aYNFDSfQrjcV6Ra7bqCGrxhtg/zZBmbgSco3aTxKOEPzDXKK+6seAAfsbIXw=="],
"@hookform/resolvers": ["@hookform/resolvers@5.2.2", "", { "dependencies": { "@standard-schema/utils": "^0.3.0" }, "peerDependencies": { "react-hook-form": "^7.55.0" } }, "sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA=="],
- "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
-
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
@@ -263,14 +262,6 @@
"@mjackson/node-fetch-server": ["@mjackson/node-fetch-server@0.2.0", "", {}, "sha512-EMlH1e30yzmTpGLQjlFmaDAjyOeZhng1/XCd7DExR8PNAnG/G1tyruZxEoUe11ClnwGhGrtsdnyyUx1frSzjng=="],
- "@npmcli/git": ["@npmcli/git@4.1.0", "", { "dependencies": { "@npmcli/promise-spawn": "^6.0.0", "lru-cache": "^7.4.4", "npm-pick-manifest": "^8.0.0", "proc-log": "^3.0.0", "promise-inflight": "^1.0.1", "promise-retry": "^2.0.1", "semver": "^7.3.5", "which": "^3.0.0" } }, "sha512-9hwoB3gStVfa0N31ymBmrX+GuDGdVA/QWShZVqE0HK2Af+7QGGrCTbZia/SW0ImUTjTne7SP91qxDmtXvDHRPQ=="],
-
- "@npmcli/package-json": ["@npmcli/package-json@4.0.1", "", { "dependencies": { "@npmcli/git": "^4.1.0", "glob": "^10.2.2", "hosted-git-info": "^6.1.1", "json-parse-even-better-errors": "^3.0.0", "normalize-package-data": "^5.0.0", "proc-log": "^3.0.0", "semver": "^7.5.3" } }, "sha512-lRCEGdHZomFsURroh522YvA/2cVb9oPIJrjHanCJZkiasz1BzcnLr3tBJhlV7S86MBJBuAQ33is2D60YitZL2Q=="],
-
- "@npmcli/promise-spawn": ["@npmcli/promise-spawn@6.0.2", "", { "dependencies": { "which": "^3.0.0" } }, "sha512-gGq0NJkIGSwdbUt4yhdF8ZrmkGKVz9vAdVzpOfnom+V8PLSmSOVhZwbNvZZS1EYcJN5hzzKBxmmVVAInM6HQLg=="],
-
- "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
-
"@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="],
"@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="],
@@ -367,13 +358,13 @@
"@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="],
- "@react-router/dev": ["@react-router/dev@7.9.5", "", { "dependencies": { "@babel/core": "^7.27.7", "@babel/generator": "^7.27.5", "@babel/parser": "^7.27.7", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/preset-typescript": "^7.27.1", "@babel/traverse": "^7.27.7", "@babel/types": "^7.27.7", "@npmcli/package-json": "^4.0.1", "@react-router/node": "7.9.5", "@remix-run/node-fetch-server": "^0.9.0", "arg": "^5.0.1", "babel-dead-code-elimination": "^1.0.6", "chokidar": "^4.0.0", "dedent": "^1.5.3", "es-module-lexer": "^1.3.1", "exit-hook": "2.2.1", "isbot": "^5.1.11", "jsesc": "3.0.2", "lodash": "^4.17.21", "p-map": "^7.0.3", "pathe": "^1.1.2", "picocolors": "^1.1.1", "prettier": "^3.6.2", "react-refresh": "^0.14.0", "semver": "^7.3.7", "tinyglobby": "^0.2.14", "valibot": "^1.1.0", "vite-node": "^3.2.2" }, "peerDependencies": { "@react-router/serve": "^7.9.5", "@vitejs/plugin-rsc": "*", "react-router": "^7.9.5", "typescript": "^5.1.0", "vite": "^5.1.0 || ^6.0.0 || ^7.0.0", "wrangler": "^3.28.2 || ^4.0.0" }, "optionalPeers": ["@react-router/serve", "@vitejs/plugin-rsc", "typescript", "wrangler"], "bin": { "react-router": "bin.js" } }, "sha512-MkWI4zN7VbQ0tteuJtX5hmDINNS26IW236a8lM8+o1344xdnT/ZsBvcUh8AkzDdCRYEz1blgzgirpj0Wc1gmXg=="],
+ "@react-router/dev": ["@react-router/dev@7.10.0", "", { "dependencies": { "@babel/core": "^7.27.7", "@babel/generator": "^7.27.5", "@babel/parser": "^7.27.7", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/preset-typescript": "^7.27.1", "@babel/traverse": "^7.27.7", "@babel/types": "^7.27.7", "@react-router/node": "7.10.0", "@remix-run/node-fetch-server": "^0.9.0", "arg": "^5.0.1", "babel-dead-code-elimination": "^1.0.6", "chokidar": "^4.0.0", "dedent": "^1.5.3", "es-module-lexer": "^1.3.1", "exit-hook": "2.2.1", "isbot": "^5.1.11", "jsesc": "3.0.2", "lodash": "^4.17.21", "p-map": "^7.0.3", "pathe": "^1.1.2", "picocolors": "^1.1.1", "pkg-types": "^2.3.0", "prettier": "^3.6.2", "react-refresh": "^0.14.0", "semver": "^7.3.7", "tinyglobby": "^0.2.14", "valibot": "^1.1.0", "vite-node": "^3.2.2" }, "peerDependencies": { "@react-router/serve": "^7.10.0", "@vitejs/plugin-rsc": "*", "react-router": "^7.10.0", "typescript": "^5.1.0", "vite": "^5.1.0 || ^6.0.0 || ^7.0.0", "wrangler": "^3.28.2 || ^4.0.0" }, "optionalPeers": ["@react-router/serve", "@vitejs/plugin-rsc", "typescript", "wrangler"], "bin": { "react-router": "bin.js" } }, "sha512-3UgkV0N5lp3+Ol3q64L4ZHgPXv2XA4KHJ59MVLSK2prokrOrPaYvqbdx40r602M+hRZp/u04ln2A6cOfBW6kxA=="],
- "@react-router/express": ["@react-router/express@7.9.5", "", { "dependencies": { "@react-router/node": "7.9.5" }, "peerDependencies": { "express": "^4.17.1 || ^5", "react-router": "7.9.5", "typescript": "^5.1.0" }, "optionalPeers": ["typescript"] }, "sha512-Mg94Tw9JSaRuwkvIC6PaODRzsLs6mo70ppz5qdIK/G3iotSxsH08TDNdzot7CaXXevk/pIiD/+Tbn0H/asHsYA=="],
+ "@react-router/express": ["@react-router/express@7.10.0", "", { "dependencies": { "@react-router/node": "7.10.0" }, "peerDependencies": { "express": "^4.17.1 || ^5", "react-router": "7.10.0", "typescript": "^5.1.0" }, "optionalPeers": ["typescript"] }, "sha512-3cBJ2cyHn5J+wSNFn+XdNSpXVAlQ+nbj7CMa3OsiEpFb+d0GLthirvSESqRjX2Eid94xNHICqKpYS9bR4QqIxg=="],
- "@react-router/node": ["@react-router/node@7.9.5", "", { "dependencies": { "@mjackson/node-fetch-server": "^0.2.0" }, "peerDependencies": { "react-router": "7.9.5", "typescript": "^5.1.0" }, "optionalPeers": ["typescript"] }, "sha512-3mDd32mXh3gEkG0cLPnUaoLkY1pApsTPqn7O1j+P8aLf997uYz5lYDjt33vtMhaotlRM0x+5JziAKtz/76YBpQ=="],
+ "@react-router/node": ["@react-router/node@7.10.0", "", { "dependencies": { "@mjackson/node-fetch-server": "^0.2.0" }, "peerDependencies": { "react-router": "7.10.0", "typescript": "^5.1.0" }, "optionalPeers": ["typescript"] }, "sha512-pff3Xz3gASrIUUX54QdlPzasdN9XRLnzoFEwUVsH5y2sZ6vijQdjZExLS6aQhPiuUr/uVPwN2WngO0Ryfrxulg=="],
- "@react-router/serve": ["@react-router/serve@7.9.5", "", { "dependencies": { "@mjackson/node-fetch-server": "^0.2.0", "@react-router/express": "7.9.5", "@react-router/node": "7.9.5", "compression": "^1.7.4", "express": "^4.19.2", "get-port": "5.1.1", "morgan": "^1.10.0", "source-map-support": "^0.5.21" }, "peerDependencies": { "react-router": "7.9.5" }, "bin": { "react-router-serve": "bin.js" } }, "sha512-sww8oDNqz8SgaXEQ3maqTuMlibCMpmWvLE0s5zyEyOQb1G99clYMcXceQ2HNU2jtXJkp+P5XI1CngpGpngyTnw=="],
+ "@react-router/serve": ["@react-router/serve@7.10.0", "", { "dependencies": { "@mjackson/node-fetch-server": "^0.2.0", "@react-router/express": "7.10.0", "@react-router/node": "7.10.0", "compression": "^1.7.4", "express": "^4.19.2", "get-port": "5.1.1", "morgan": "^1.10.0", "source-map-support": "^0.5.21" }, "peerDependencies": { "react-router": "7.10.0" }, "bin": { "react-router-serve": "bin.js" } }, "sha512-tgdbw1lmDkzF3gCMj//iNklgUrYHUxz35rj0sbyLeti8K2gVsNxaZWyt5omanFgkeZ7WYfi0wzLHviqxl228eA=="],
"@reduxjs/toolkit": ["@reduxjs/toolkit@2.10.1", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@standard-schema/utils": "^0.3.0", "immer": "^10.2.0", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.0" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" }, "optionalPeers": ["react", "react-redux"] }, "sha512-/U17EXQ9Do9Yx4DlNGU6eVNfZvFJfYpUtRRdLf19PbPjdWBxNlxGZXywQZ1p1Nz8nMkWplTI7iD/23m07nolDA=="],
@@ -423,13 +414,11 @@
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.53.2", "", { "os": "win32", "cpu": "x64" }, "sha512-k+/Rkcyx//P6fetPoLMb8pBeqJBNGx81uuf7iljX9++yNBVRDQgD04L+SVXmXmh5ZP4/WOp4mWF0kmi06PW2tA=="],
- "@scalar/core": ["@scalar/core@0.3.22", "", { "dependencies": { "@scalar/types": "0.4.0" } }, "sha512-6lzeRkvgkukSgge35kvxJKiJBny4rdGSaLTNzn/sF1F6JRfUo7I0AgqFxxSZWMD+EG4kGyNxAz0zciDSx2Cjvw=="],
+ "@scalar/core": ["@scalar/core@0.3.23", "", { "dependencies": { "@scalar/types": "0.5.0" } }, "sha512-hop7LVR3MKB2VpS8dly3gmmbB3lBGxQRtL0pBaC77zFMRHoBv1DuB2bj8l4gxd5grzitJ1LsYduvywLAMY9F6g=="],
- "@scalar/hono-api-reference": ["@scalar/hono-api-reference@0.9.24", "", { "dependencies": { "@scalar/core": "0.3.22" }, "peerDependencies": { "hono": "^4.10.3" } }, "sha512-NjPY3iMm/FqYRXAgr6V7qBhJGbSUQ8hbijFUMuqZo4pIjGEUNLeB5L9U2Gh4cDIPPWeso8mlc16jaX7dV0FrPw=="],
+ "@scalar/hono-api-reference": ["@scalar/hono-api-reference@0.9.25", "", { "dependencies": { "@scalar/core": "0.3.23" }, "peerDependencies": { "hono": "^4.10.3" } }, "sha512-ZEQAhvVU/FXdJs8+rVXdfWjwzkE+M6Zr+4W+zNhy8DF17BIpxFXfVL7i3OxK1V/4EtkTplkETjYGTR4ju3RFZw=="],
- "@scalar/openapi-types": ["@scalar/openapi-types@0.5.1", "", { "dependencies": { "zod": "4.1.11" } }, "sha512-8g7s9lPolyDFtijyh3Ob459tpezPuZbkXoFgJwBTHjPZ7ap+TvOJTvLk56CFwxVBVz2BxCzWJqxYyy3FUdeLoA=="],
-
- "@scalar/types": ["@scalar/types@0.4.0", "", { "dependencies": { "@scalar/openapi-types": "0.5.1", "nanoid": "5.1.5", "type-fest": "5.0.0", "zod": "4.1.11" } }, "sha512-vOD1GZez7kPdVA+UQit05QE9dbALfevhK9kqRTsqcPX7FvvZ9eQWSNl1GKmKtmRiAZGThv2agM5AvHRxkH2JSw=="],
+ "@scalar/types": ["@scalar/types@0.5.0", "", { "dependencies": { "nanoid": "5.1.5", "type-fest": "5.0.0", "zod": "4.1.11" } }, "sha512-imDMuTieOc5kHM9/Kt/1lmiI5ZtusuaYlzsXTP99IsWvD8mJ7ivF73lPBRj4PKtg4vY+ta5CO/vJpvnCYandRg=="],
"@so-ric/colorspace": ["@so-ric/colorspace@1.1.6", "", { "dependencies": { "color": "^5.0.2", "text-hex": "1.0.x" } }, "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw=="],
@@ -471,15 +460,15 @@
"@tailwindcss/vite": ["@tailwindcss/vite@4.1.17", "", { "dependencies": { "@tailwindcss/node": "4.1.17", "@tailwindcss/oxide": "4.1.17", "tailwindcss": "4.1.17" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-4+9w8ZHOiGnpcGI6z1TVVfWaX/koK7fKeSYF3qlYg2xpBtbteP2ddBxiarL+HVgfSJGeK5RIxRQmKm4rTJJAwA=="],
- "@tanstack/query-core": ["@tanstack/query-core@5.90.8", "", {}, "sha512-4E0RP/0GJCxSNiRF2kAqE/LQkTJVlL/QNU7gIJSptaseV9HP6kOuA+N11y4bZKZxa3QopK3ZuewwutHx6DqDXQ=="],
+ "@tanstack/query-core": ["@tanstack/query-core@5.90.11", "", {}, "sha512-f9z/nXhCgWDF4lHqgIE30jxLe4sYv15QodfdPDKYAk7nAEjNcndy4dHz3ezhdUaR23BpWa4I2EH4/DZ0//Uf8A=="],
- "@tanstack/query-devtools": ["@tanstack/query-devtools@5.90.1", "", {}, "sha512-GtINOPjPUH0OegJExZ70UahT9ykmAhmtNVcmtdnOZbxLwT7R5OmRztR5Ahe3/Cu7LArEmR6/588tAycuaWb1xQ=="],
+ "@tanstack/query-devtools": ["@tanstack/query-devtools@5.91.1", "", {}, "sha512-l8bxjk6BMsCaVQH6NzQEE/bEgFy1hAs5qbgXl0xhzezlaQbPk6Mgz9BqEg2vTLPOHD8N4k+w/gdgCbEzecGyNg=="],
- "@tanstack/react-query": ["@tanstack/react-query@5.90.8", "", { "dependencies": { "@tanstack/query-core": "5.90.8" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-/3b9QGzkf4rE5/miL6tyhldQRlLXzMHcySOm/2Tm2OLEFE9P1ImkH0+OviDBSvyAvtAOJocar5xhd7vxdLi3aQ=="],
+ "@tanstack/react-query": ["@tanstack/react-query@5.90.11", "", { "dependencies": { "@tanstack/query-core": "5.90.11" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-3uyzz01D1fkTLXuxF3JfoJoHQMU2fxsfJwE+6N5hHy0dVNoZOvwKP8Z2k7k1KDeD54N20apcJnG75TBAStIrBA=="],
- "@tanstack/react-query-devtools": ["@tanstack/react-query-devtools@5.90.2", "", { "dependencies": { "@tanstack/query-devtools": "5.90.1" }, "peerDependencies": { "@tanstack/react-query": "^5.90.2", "react": "^18 || ^19" } }, "sha512-vAXJzZuBXtCQtrY3F/yUNJCV4obT/A/n81kb3+YqLbro5Z2+phdAbceO+deU3ywPw8B42oyJlp4FhO0SoivDFQ=="],
+ "@tanstack/react-query-devtools": ["@tanstack/react-query-devtools@5.91.1", "", { "dependencies": { "@tanstack/query-devtools": "5.91.1" }, "peerDependencies": { "@tanstack/react-query": "^5.90.10", "react": "^18 || ^19" } }, "sha512-tRnJYwEbH0kAOuToy8Ew7bJw1lX3AjkkgSlf/vzb+NpnqmHPdWM+lA2DSdGQSLi1SU0PDRrrCI1vnZnci96CsQ=="],
- "@types/bun": ["@types/bun@1.3.2", "", { "dependencies": { "bun-types": "1.3.2" } }, "sha512-t15P7k5UIgHKkxwnMNkJbWlh/617rkDGEdSsDbu+qNHTaz9SKf7aC8fiIlUdD5RPpH6GEkP0cK7WlvmrEBRtWg=="],
+ "@types/bun": ["@types/bun@1.3.3", "", { "dependencies": { "bun-types": "1.3.3" } }, "sha512-ogrKbJ2X5N0kWLLFKeytG0eHDleBYtngtlbu9cyBKFtNL3cnpDZkNdQj8flVf6WTZUX5ulI9AY1oa7ljhSrp+g=="],
"@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="],
@@ -501,7 +490,7 @@
"@types/docker-modem": ["@types/docker-modem@3.0.6", "", { "dependencies": { "@types/node": "*", "@types/ssh2": "*" } }, "sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg=="],
- "@types/dockerode": ["@types/dockerode@3.3.45", "", { "dependencies": { "@types/docker-modem": "*", "@types/node": "*", "@types/ssh2": "*" } }, "sha512-iYpZF+xr5QLpIICejLdUF2r5gh8IXY1Gw3WLmt41dUbS3Vn/3hVgL+6lJBVbmrhYBWfbWPPstdr6+A0s95DTWA=="],
+ "@types/dockerode": ["@types/dockerode@3.3.47", "", { "dependencies": { "@types/docker-modem": "*", "@types/node": "*", "@types/ssh2": "*" } }, "sha512-ShM1mz7rCjdssXt7Xz0u1/R2BJC7piWa3SJpUBiVjCf2A3XNn4cP6pUVaD8bLanpPVVn4IKzJuw3dOvkJ8IbYw=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
@@ -509,7 +498,7 @@
"@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="],
- "@types/react": ["@types/react@19.2.4", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-tBFxBp9Nfyy5rsmefN+WXc1JeW/j2BpBHFdLZbEVfs9wn3E3NRFxwV0pJg8M1qQAexFpvz73hJXFofV0ZAu92A=="],
+ "@types/react": ["@types/react@19.2.7", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg=="],
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
@@ -533,9 +522,9 @@
"aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="],
- "arkregex": ["arkregex@0.0.2", "", { "dependencies": { "@ark/util": "0.53.0" } }, "sha512-ttjDUICBVoXD/m8bf7eOjx8XMR6yIT2FmmW9vsN0FCcFOygEZvvIX8zK98tTdXkzi0LkRi5CmadB44jFEIyDNA=="],
+ "arkregex": ["arkregex@0.0.4", "", { "dependencies": { "@ark/util": "0.56.0" } }, "sha512-biS/FkvSwQq59TZ453piUp8bxMui11pgOMV9WHAnli1F8o0ayNCZzUwQadL/bGIUic5TkS/QlPcyMuI8ZIwedQ=="],
- "arktype": ["arktype@2.1.26", "", { "dependencies": { "@ark/schema": "0.54.0", "@ark/util": "0.54.0", "arkregex": "0.0.2" } }, "sha512-zDwukKV6uTElKCAbIoQ9OU6shXE5ALjvZAqHErOSv6l0iLKlubELZ7AcevYLaWFYr5rmIN4Uv9+dIzInktSO1A=="],
+ "arktype": ["arktype@2.1.28", "", { "dependencies": { "@ark/schema": "0.56.0", "@ark/util": "0.56.0", "arkregex": "0.0.4" } }, "sha512-LVZqXl2zWRpNFnbITrtFmqeqNkPPo+KemuzbGSY6jvJwCb4v8NsDzrWOLHnQgWl26TkJeWWcUNUeBpq2Mst1/Q=="],
"array-flatten": ["array-flatten@1.1.1", "", {}, "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="],
@@ -569,7 +558,7 @@
"buildcheck": ["buildcheck@0.0.6", "", {}, "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A=="],
- "bun-types": ["bun-types@1.3.2", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-i/Gln4tbzKNuxP70OWhJRZz1MRfvqExowP7U6JKoI8cntFrtxg7RJK3jvz7wQW54UuvNC8tbKHHri5fy74FVqg=="],
+ "bun-types": ["bun-types@1.3.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ=="],
"bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
@@ -607,7 +596,7 @@
"color-support": ["color-support@1.1.3", "", { "bin": { "color-support": "bin.js" } }, "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg=="],
- "commander": ["commander@14.0.1", "", {}, "sha512-2JkV3gUZUVrbNA+1sjBOYLsMZ5cEEl8GTFP2a4AVz5hvasAMCQ1D2l2le/cX+pV4N6ZU17zjUahLpIXRrnWL8A=="],
+ "commander": ["commander@14.0.2", "", {}, "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ=="],
"compressible": ["compressible@2.0.18", "", { "dependencies": { "mime-db": ">= 1.43.0 < 2" } }, "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg=="],
@@ -631,9 +620,7 @@
"cron-parser": ["cron-parser@5.4.0", "", { "dependencies": { "luxon": "^3.7.1" } }, "sha512-HxYB8vTvnQFx4dLsZpGRa0uHp6X3qIzS3ZJgJ9v6l/5TJMgeWQbLkR5yiJ5hOxGbc9+jCADDnydIe15ReLZnJA=="],
- "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
-
- "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
+ "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
"d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="],
@@ -665,7 +652,7 @@
"dedent": ["dedent@1.7.0", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ=="],
- "default-browser": ["default-browser@5.3.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-Qq68+VkJlc8tjnPV1i7HtbIn7ohmjZa88qUvHMIK0ZKUXMCuV45cT7cEXALPUmeXCe0q1DWQkQTemHVaLIFSrg=="],
+ "default-browser": ["default-browser@5.4.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg=="],
"default-browser-id": ["default-browser-id@5.0.0", "", {}, "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA=="],
@@ -691,14 +678,12 @@
"dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="],
- "drizzle-kit": ["drizzle-kit@0.31.6", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "esbuild-register": "^3.5.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-/B4e/4pwnx25QwD5xXgdpo1S+077a2VZdosXbItE/oNmUgQwZydGDz9qJYmnQl/b+5IX0rLfwRhrPnroGtrg8Q=="],
+ "drizzle-kit": ["drizzle-kit@0.31.7", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "esbuild-register": "^3.5.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-hOzRGSdyKIU4FcTSFYGKdXEjFsncVwHZ43gY3WU5Bz9j5Iadp6Rh6hxLSQ1IWXpKLBKt/d5y1cpSPcV+FcoQ1A=="],
"drizzle-orm": ["drizzle-orm@0.44.7", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-quIpnYznjU9lHshEOAYLoZ9s3jweleHlZIAWR/jX9gAWNg/JhQ1wj0KGRf7/Zm+obRrYd9GjPVJg790QY9N5AQ=="],
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
- "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
-
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
"electron-to-chromium": ["electron-to-chromium@1.5.250", "", {}, "sha512-/5UMj9IiGDMOFBnN4i7/Ry5onJrAGSbOGo3s9FEKmwobGq6xw832ccET0CE3CkkMBZ8GJSlUIesZofpyurqDXw=="],
@@ -713,8 +698,6 @@
"enhanced-resolve": ["enhanced-resolve@5.18.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww=="],
- "err-code": ["err-code@2.0.3", "", {}, "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA=="],
-
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
@@ -723,7 +706,7 @@
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
- "es-toolkit": ["es-toolkit@1.41.0", "", {}, "sha512-bDd3oRmbVgqZCJS6WmeQieOrzpl3URcWBUVDXxOELlUW2FuW+0glPOz1n0KnRie+PdyvUZcXz2sOn00c6pPRIA=="],
+ "es-toolkit": ["es-toolkit@1.42.0", "", {}, "sha512-SLHIyY7VfDJBM8clz4+T2oquwTQxEzu263AyhVK4jREOAwJ+8eebaa4wM3nlvnAqhDrMm2EsA6hWHaQsMPQ1nA=="],
"esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
@@ -751,8 +734,6 @@
"fn.name": ["fn.name@1.1.0", "", {}, "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="],
- "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
-
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
"fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="],
@@ -779,8 +760,6 @@
"giget": ["giget@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA=="],
- "glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="],
-
"globrex": ["globrex@0.1.2", "", {}, "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg=="],
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
@@ -795,11 +774,9 @@
"hono-openapi": ["hono-openapi@1.1.1", "", { "peerDependencies": { "@hono/standard-validator": "^0.1.2", "@standard-community/standard-json": "^0.3.5", "@standard-community/standard-openapi": "^0.2.8", "@types/json-schema": "^7.0.15", "hono": "^4.8.3", "openapi-types": "^12.1.3" }, "optionalPeers": ["@hono/standard-validator", "hono"] }, "sha512-AC3HNhZYPHhnZdSy2Je7GDoTTNxPos6rKRQKVDBbSilY3cWJPqsxRnN6zA4pU7tfxmQEMTqkiLXbw6sAaemB8Q=="],
- "hosted-git-info": ["hosted-git-info@6.1.3", "", { "dependencies": { "lru-cache": "^7.5.1" } }, "sha512-HVJyzUrLIL1c0QmviVh5E8VGyUS7xCFPS6yydaVd1UegW+ibV/CohqTH9MkOLDp5o+rb82DMo77PTuc9F/8GKw=="],
-
"http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="],
- "http-errors-enhanced": ["http-errors-enhanced@3.0.2", "", {}, "sha512-Pl8CVX7eu46fYSfEGCnOGD8aZkd/2dtr9214BpNRDiU1THT7vsu+T87SDQZgxjOGmsNXaTZe1sqIo/oZgJIfow=="],
+ "http-errors-enhanced": ["http-errors-enhanced@4.0.2", "", {}, "sha512-5EXN1gmhJVvuWpNfz+RclWvLnnENEXNMPfww3gm30H9mQzPF4QSBj/MD5FRkVDxGIUhO/cR2GSLCd/6C6xpBcw=="],
"iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
@@ -813,12 +790,12 @@
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
- "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="],
-
"is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="],
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
+ "is-in-ssh": ["is-in-ssh@1.0.0", "", {}, "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw=="],
+
"is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="],
"is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
@@ -827,10 +804,6 @@
"isbot": ["isbot@5.1.32", "", {}, "sha512-VNfjM73zz2IBZmdShMfAUg10prm6t7HFUQmNAEOAVS4YH92ZrZcvkMcGX6cIgBJAzWDzPent/EeAtYEHNPNPBQ=="],
- "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
-
- "jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
-
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
@@ -839,8 +812,6 @@
"jsesc": ["jsesc@3.0.2", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g=="],
- "json-parse-even-better-errors": ["json-parse-even-better-errors@3.0.2", "", {}, "sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ=="],
-
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
"kuler": ["kuler@2.0.0", "", {}, "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="],
@@ -879,7 +850,7 @@
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
- "lucide-react": ["lucide-react@0.546.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Z94u6fKT43lKeYHiVyvyR8fT7pwCzDu7RyMPpTvh054+xahSgj4HFQ+NmflvzdXsoAjYGdCguGaFKYuvq0ThCQ=="],
+ "lucide-react": ["lucide-react@0.555.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-D8FvHUGbxWBRQM90NZeIyhAvkFfsh3u9ekrMvJ30Z6gnpBHS6HC6ldLg7tL45hwiIz/u66eKDtdA23gwwGsAHA=="],
"luxon": ["luxon@3.7.2", "", {}, "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew=="],
@@ -901,8 +872,6 @@
"minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
- "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
-
"mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="],
"morgan": ["morgan@1.10.1", "", { "dependencies": { "basic-auth": "~2.0.1", "debug": "2.6.9", "depd": "~2.0.0", "on-finished": "~2.3.0", "on-headers": "~1.1.0" } }, "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A=="],
@@ -923,16 +892,6 @@
"node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="],
- "normalize-package-data": ["normalize-package-data@5.0.0", "", { "dependencies": { "hosted-git-info": "^6.0.0", "is-core-module": "^2.8.1", "semver": "^7.3.5", "validate-npm-package-license": "^3.0.4" } }, "sha512-h9iPVIfrVZ9wVYQnxFgtw1ugSvGEMOlyPWWtm8BMJhnwyEL/FLbYbTY3V3PpjI/BUK67n9PEWDu6eHzu1fB15Q=="],
-
- "npm-install-checks": ["npm-install-checks@6.3.0", "", { "dependencies": { "semver": "^7.1.1" } }, "sha512-W29RiK/xtpCGqn6f3ixfRYGk+zRyr+Ew9F2E20BfXxT5/euLdA/Nm7fO7OeTGuAmTs30cpgInyJ0cYe708YTZw=="],
-
- "npm-normalize-package-bin": ["npm-normalize-package-bin@3.0.1", "", {}, "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ=="],
-
- "npm-package-arg": ["npm-package-arg@10.1.0", "", { "dependencies": { "hosted-git-info": "^6.0.0", "proc-log": "^3.0.0", "semver": "^7.3.5", "validate-npm-package-name": "^5.0.0" } }, "sha512-uFyyCEmgBfZTtrKk/5xDfHp6+MdrqGotX/VoOyEEl3mBwiEE5FlBaePanazJSVMPT7vKepcjYBY2ztg9A3yPIA=="],
-
- "npm-pick-manifest": ["npm-pick-manifest@8.0.2", "", { "dependencies": { "npm-install-checks": "^6.0.0", "npm-normalize-package-bin": "^3.0.0", "npm-package-arg": "^10.0.0", "semver": "^7.3.5" } }, "sha512-1dKY+86/AIiq1tkKVD3l0WI+Gd3vkknVGAggsFeBkTvbhMQ1OND/LKkYv4JtXPKUJ8bOTCyLiqEg2P6QNdK+Gg=="],
-
"nypm": ["nypm@0.6.2", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.2", "pathe": "^2.0.3", "pkg-types": "^2.3.0", "tinyexec": "^1.0.1" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g=="],
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
@@ -947,20 +906,14 @@
"one-time": ["one-time@1.0.0", "", { "dependencies": { "fn.name": "1.x.x" } }, "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g=="],
- "open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="],
+ "open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="],
"openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="],
"p-map": ["p-map@7.0.4", "", {}, "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ=="],
- "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="],
-
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
- "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
-
- "path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
-
"path-to-regexp": ["path-to-regexp@0.1.12", "", {}, "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="],
"pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="],
@@ -975,14 +928,10 @@
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
+ "powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="],
+
"prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="],
- "proc-log": ["proc-log@3.0.0", "", {}, "sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A=="],
-
- "promise-inflight": ["promise-inflight@1.0.1", "", {}, "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g=="],
-
- "promise-retry": ["promise-retry@2.0.1", "", { "dependencies": { "err-code": "^2.0.2", "retry": "^0.12.0" } }, "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g=="],
-
"protobufjs": ["protobufjs@7.5.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="],
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
@@ -999,11 +948,11 @@
"rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="],
- "react": ["react@19.2.0", "", {}, "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ=="],
+ "react": ["react@19.2.1", "", {}, "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw=="],
- "react-dom": ["react-dom@19.2.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ=="],
+ "react-dom": ["react-dom@19.2.1", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.1" } }, "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg=="],
- "react-hook-form": ["react-hook-form@7.66.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-xXBqsWGKrY46ZqaHDo+ZUYiMUgi8suYu5kdrS20EG8KiL7VRQitEbNjm+UcrDYrNi1YLyfpmAeGjCZYXLT9YBw=="],
+ "react-hook-form": ["react-hook-form@7.68.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-oNN3fjrZ/Xo40SWlHf1yCjlMK417JxoSJVUXQjGdvdRCU07NTFei1i1f8ApUAts+IVh14e4EdakeLEA+BEAs/Q=="],
"react-is": ["react-is@19.2.0", "", {}, "sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA=="],
@@ -1015,7 +964,7 @@
"react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
- "react-router": ["react-router@7.9.5", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-JmxqrnBZ6E9hWmf02jzNn9Jm3UqyeimyiwzD69NjxGySG6lIz/1LVPsoTCwN7NBX2XjCEa1LIX5EMz1j2b6u6A=="],
+ "react-router": ["react-router@7.10.0", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-FVyCOH4IZ0eDDRycODfUqoN8ZSR2LbTvtx6RPsBgzvJ8xAXlMZNCrOFpu+jb8QbtZnpAd/cEki2pwE848pNGxw=="],
"react-router-hono-server": ["react-router-hono-server@2.22.0", "", { "dependencies": { "@drizzle-team/brocli": "^0.11.0", "@hono/node-server": "^1.19.5", "@hono/node-ws": "^1.2.0", "@hono/vite-dev-server": "^0.23.0", "hono": "^4.10.3" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20250317.0", "@react-router/dev": "^7.9.0", "@types/react": "^19.0.0", "miniflare": "^3.20241205.0", "react-router": "^7.9.0", "vite": "^7.0.0", "wrangler": "^4.2.0" }, "optionalPeers": ["@cloudflare/workers-types", "miniflare", "wrangler"], "bin": { "react-router-hono-server": "dist/cli.js" } }, "sha512-XPJp1PQtkjHsFrneUdvmv22o7LPBGYEW11KtTDEmpXQINW5ViwpzUuwb0/eGjZ9E4RQh6AOKYsXA4+QIqXTkrQ=="],
@@ -1025,7 +974,7 @@
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
- "recharts": ["recharts@3.2.1", "", { "dependencies": { "@reduxjs/toolkit": "1.x.x || 2.x.x", "clsx": "^2.1.1", "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", "eventemitter3": "^5.0.1", "immer": "^10.1.1", "react-redux": "8.x.x || 9.x.x", "reselect": "5.1.1", "tiny-invariant": "^1.3.3", "use-sync-external-store": "^1.2.2", "victory-vendor": "^37.0.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-0JKwHRiFZdmLq/6nmilxEZl3pqb4T+aKkOkOi/ZISRZwfBhVMgInxzlYU9D4KnCH3KINScLy68m/OvMXoYGZUw=="],
+ "recharts": ["recharts@3.5.1", "", { "dependencies": { "@reduxjs/toolkit": "1.x.x || 2.x.x", "clsx": "^2.1.1", "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", "eventemitter3": "^5.0.1", "immer": "^10.1.1", "react-redux": "8.x.x || 9.x.x", "reselect": "5.1.1", "tiny-invariant": "^1.3.3", "use-sync-external-store": "^1.2.2", "victory-vendor": "^37.0.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-+v+HJojK7gnEgG6h+b2u7k8HH7FhyFUzAc4+cPrsjL4Otdgqr/ecXzAnHciqlzV1ko064eNcsdzrYOM78kankA=="],
"redux": ["redux@5.0.1", "", {}, "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="],
@@ -1037,8 +986,6 @@
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
- "retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="],
-
"rollup": ["rollup@4.53.2", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.53.2", "@rollup/rollup-android-arm64": "4.53.2", "@rollup/rollup-darwin-arm64": "4.53.2", "@rollup/rollup-darwin-x64": "4.53.2", "@rollup/rollup-freebsd-arm64": "4.53.2", "@rollup/rollup-freebsd-x64": "4.53.2", "@rollup/rollup-linux-arm-gnueabihf": "4.53.2", "@rollup/rollup-linux-arm-musleabihf": "4.53.2", "@rollup/rollup-linux-arm64-gnu": "4.53.2", "@rollup/rollup-linux-arm64-musl": "4.53.2", "@rollup/rollup-linux-loong64-gnu": "4.53.2", "@rollup/rollup-linux-ppc64-gnu": "4.53.2", "@rollup/rollup-linux-riscv64-gnu": "4.53.2", "@rollup/rollup-linux-riscv64-musl": "4.53.2", "@rollup/rollup-linux-s390x-gnu": "4.53.2", "@rollup/rollup-linux-x64-gnu": "4.53.2", "@rollup/rollup-linux-x64-musl": "4.53.2", "@rollup/rollup-openharmony-arm64": "4.53.2", "@rollup/rollup-win32-arm64-msvc": "4.53.2", "@rollup/rollup-win32-ia32-msvc": "4.53.2", "@rollup/rollup-win32-x64-gnu": "4.53.2", "@rollup/rollup-win32-x64-msvc": "4.53.2", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-MHngMYwGJVi6Fmnk6ISmnk7JAHRNF0UkuucA0CUW3N3a4KnONPEZz+vUanQP/ZC/iY1Qkf3bwPWzyY84wEks1g=="],
"run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="],
@@ -1061,10 +1008,6 @@
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
- "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
-
- "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
-
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
"side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
@@ -1073,8 +1016,6 @@
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
- "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
-
"slugify": ["slugify@1.6.6", "", {}, "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw=="],
"sonner": ["sonner@2.0.7", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w=="],
@@ -1085,14 +1026,6 @@
"source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="],
- "spdx-correct": ["spdx-correct@3.2.0", "", { "dependencies": { "spdx-expression-parse": "^3.0.0", "spdx-license-ids": "^3.0.0" } }, "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA=="],
-
- "spdx-exceptions": ["spdx-exceptions@2.5.0", "", {}, "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w=="],
-
- "spdx-expression-parse": ["spdx-expression-parse@3.0.1", "", { "dependencies": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" } }, "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q=="],
-
- "spdx-license-ids": ["spdx-license-ids@3.0.22", "", {}, "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ=="],
-
"split-ca": ["split-ca@1.0.1", "", {}, "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ=="],
"ssh2": ["ssh2@1.17.0", "", { "dependencies": { "asn1": "^0.2.6", "bcrypt-pbkdf": "^1.0.2" }, "optionalDependencies": { "cpu-features": "~0.0.10", "nan": "^2.23.0" } }, "sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ=="],
@@ -1103,14 +1036,10 @@
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
- "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
-
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
- "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
-
"tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="],
"tailwind-merge": ["tailwind-merge@3.4.0", "", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="],
@@ -1171,15 +1100,11 @@
"valibot": ["valibot@1.1.0", "", { "peerDependencies": { "typescript": ">=5" }, "optionalPeers": ["typescript"] }, "sha512-Nk8lX30Qhu+9txPYTwM0cFlWLdPFsFr6LblzqIySfbZph9+BFsAHsNvHOymEviUepeIW6KFHzpX8TKhbptBXXw=="],
- "validate-npm-package-license": ["validate-npm-package-license@3.0.4", "", { "dependencies": { "spdx-correct": "^3.0.0", "spdx-expression-parse": "^3.0.0" } }, "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew=="],
-
- "validate-npm-package-name": ["validate-npm-package-name@5.0.1", "", {}, "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ=="],
-
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
"victory-vendor": ["victory-vendor@37.3.6", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ=="],
- "vite": ["vite@7.2.2", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ=="],
+ "vite": ["vite@7.2.6", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ=="],
"vite-bundle-analyzer": ["vite-bundle-analyzer@1.2.3", "", { "bin": { "analyze": "dist/bin.js" } }, "sha512-8nhwDGHWMKKgg6oegAOpDgTT7/yzTVzeYzLF4y8WBJoYu9gO7h29UpHiQnXD2rAvfQzDy5Wqe/Za5cgqhnxi5g=="],
@@ -1187,27 +1112,23 @@
"vite-tsconfig-paths": ["vite-tsconfig-paths@5.1.4", "", { "dependencies": { "debug": "^4.1.1", "globrex": "^0.1.2", "tsconfck": "^3.0.3" }, "peerDependencies": { "vite": "*" }, "optionalPeers": ["vite"] }, "sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w=="],
- "which": ["which@3.0.1", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/which.js" } }, "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg=="],
-
"winston": ["winston@3.18.3", "", { "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.8", "async": "^3.2.3", "is-stream": "^2.0.0", "logform": "^2.7.0", "one-time": "^1.0.0", "readable-stream": "^3.4.0", "safe-stable-stringify": "^2.3.1", "stack-trace": "0.0.x", "triple-beam": "^1.3.0", "winston-transport": "^4.9.0" } }, "sha512-NoBZauFNNWENgsnC9YpgyYwOVrl2m58PpQ8lNHjV3kosGs7KJ7Npk9pCUE+WJlawVSe8mykWDKWFSVfs3QO9ww=="],
"winston-transport": ["winston-transport@4.9.0", "", { "dependencies": { "logform": "^2.7.0", "readable-stream": "^3.6.2", "triple-beam": "^1.3.0" } }, "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A=="],
"wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
- "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
-
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
"ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
- "wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="],
+ "wsl-utils": ["wsl-utils@0.3.0", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-3sFIGLiaDP7rTO4xh3g+b3AzhYDIUGGywE/WsmqzJWDxus5aJXVnPTNC/6L+r2WzrwXqVOdD262OaO+cEyPMSQ=="],
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
- "yaml": ["yaml@2.8.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw=="],
+ "yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="],
"yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
@@ -1225,18 +1146,6 @@
"@grpc/grpc-js/@grpc/proto-loader": ["@grpc/proto-loader@0.8.0", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.5.3", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ=="],
- "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
-
- "@isaacs/cliui/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
-
- "@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
-
- "@npmcli/git/lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="],
-
- "@npmcli/git/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
-
- "@npmcli/package-json/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
-
"@radix-ui/react-alert-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
@@ -1279,8 +1188,6 @@
"ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
- "arkregex/@ark/util": ["@ark/util@0.53.0", "", {}, "sha512-TGn4gLlA6dJcQiqrtCtd88JhGb2XBHo6qIejsDre+nxpGuUVW4G3YZGVrwjNBTO0EyR+ykzIo4joHJzOj+/cpA=="],
-
"basic-auth/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
"body-parser/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
@@ -1289,8 +1196,6 @@
"compression/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
- "cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
-
"express/cookie": ["cookie@0.7.1", "", {}, "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w=="],
"express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
@@ -1299,26 +1204,14 @@
"giget/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
- "hosted-git-info/lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="],
-
"mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
"morgan/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
"morgan/on-finished": ["on-finished@2.3.0", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww=="],
- "normalize-package-data/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
-
- "npm-install-checks/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
-
- "npm-package-arg/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
-
- "npm-pick-manifest/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
-
"nypm/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
- "path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
-
"pkg-types/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
"react-router-hono-server/@drizzle-team/brocli": ["@drizzle-team/brocli@0.11.0", "", {}, "sha512-hD3pekGiPg0WPCCGAZmusBBJsDqGUR66Y452YgQsZOnkdQ7ViEPKuyP4huUGEZQefp8g34RRodXYmJ2TbCH+tg=="],
@@ -1329,6 +1222,8 @@
"vite-node/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
+ "vite-node/vite": ["vite@7.2.2", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ=="],
+
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="],
@@ -1373,12 +1268,6 @@
"@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="],
- "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
-
- "@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
-
- "@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
-
"@types/ssh2/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
"ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
diff --git a/docker-compose.yml b/docker-compose.yml
index 2478d6bf..689f167b 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -41,3 +41,4 @@ services:
- /var/lib/zerobyte:/var/lib/zerobyte:rshared
- /run/docker/plugins:/run/docker/plugins
- /var/run/docker.sock:/var/run/docker.sock
+ - ~/.config/rclone:/root/.config/rclone
diff --git a/package.json b/package.json
index 79bf8317..a7872574 100644
--- a/package.json
+++ b/package.json
@@ -2,14 +2,14 @@
"name": "zerobyte",
"private": true,
"type": "module",
- "packageManager": "bun@1.3.1",
+ "packageManager": "bun@1.3.3",
"scripts": {
"build": "react-router build",
"dev": "bunx --bun vite",
"start": "bun ./dist/server/index.js",
"tsc": "react-router typegen && tsc",
"lint": "biome check .",
- "lint:ci": "biome check . --ci",
+ "lint:ci": "biome ci . --changed --error-on-warnings --no-errors-on-unmatched",
"start:dev": "docker compose down && docker compose up --build zerobyte-dev",
"start:prod": "docker compose down && docker compose up --build zerobyte-prod",
"gen:api-client": "openapi-ts",
@@ -17,25 +17,25 @@
"studio": "drizzle-kit studio"
},
"dependencies": {
- "@hono/standard-validator": "^0.1.5",
+ "@hono/standard-validator": "^0.2.0",
"@hookform/resolvers": "^5.2.2",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.15",
- "@radix-ui/react-label": "^2.1.7",
+ "@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-progress": "^1.1.8",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.2.6",
- "@radix-ui/react-separator": "^1.1.7",
- "@radix-ui/react-slot": "^1.2.3",
+ "@radix-ui/react-separator": "^1.1.8",
+ "@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.8",
- "@react-router/node": "^7.9.3",
- "@react-router/serve": "^7.9.3",
- "@scalar/hono-api-reference": "^0.9.24",
- "@tanstack/react-query": "^5.90.2",
- "arktype": "^2.1.26",
+ "@react-router/node": "^7.10.0",
+ "@react-router/serve": "^7.10.0",
+ "@scalar/hono-api-reference": "^0.9.25",
+ "@tanstack/react-query": "^5.90.11",
+ "arktype": "^2.1.28",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cron-parser": "^5.4.0",
@@ -44,45 +44,45 @@
"dockerode": "^4.0.9",
"dotenv": "^17.2.3",
"drizzle-orm": "^0.44.7",
- "es-toolkit": "^1.41.0",
- "hono": "^4.10.5",
+ "es-toolkit": "^1.42.0",
+ "hono": "4.10.5",
"hono-openapi": "^1.1.1",
- "http-errors-enhanced": "^3.0.2",
- "isbot": "^5.1.31",
- "lucide-react": "^0.546.0",
+ "http-errors-enhanced": "^4.0.2",
+ "isbot": "^5.1.32",
+ "lucide-react": "^0.555.0",
"next-themes": "^0.4.6",
"node-cron": "^4.2.1",
- "react": "^19.2.0",
- "react-dom": "^19.2.0",
- "react-hook-form": "^7.63.0",
- "react-router": "^7.9.3",
+ "react": "^19.2.1",
+ "react-dom": "^19.2.1",
+ "react-hook-form": "^7.68.0",
+ "react-router": "^7.10.0",
"react-router-hono-server": "^2.22.0",
- "recharts": "3.2.1",
+ "recharts": "3.5.1",
"slugify": "^1.6.6",
"sonner": "^2.0.7",
- "tailwind-merge": "^3.3.1",
+ "tailwind-merge": "^3.4.0",
"tiny-typed-emitter": "^2.1.0",
"winston": "^3.18.3",
- "yaml": "^2.8.1"
+ "yaml": "^2.8.2"
},
"devDependencies": {
- "@biomejs/biome": "^2.3.5",
- "@hey-api/openapi-ts": "^0.87.4",
- "@react-router/dev": "^7.9.3",
- "@tailwindcss/vite": "^4.1.14",
- "@tanstack/react-query-devtools": "^5.90.2",
- "@types/bun": "^1.3.2",
- "@types/dockerode": "^3.3.45",
- "@types/node": "^24.6.2",
- "@types/react": "^19.2.0",
- "@types/react-dom": "^19.2.0",
- "drizzle-kit": "^0.31.6",
+ "@biomejs/biome": "^2.3.8",
+ "@hey-api/openapi-ts": "^0.88.0",
+ "@react-router/dev": "^7.10.0",
+ "@tailwindcss/vite": "^4.1.17",
+ "@tanstack/react-query-devtools": "^5.91.1",
+ "@types/bun": "^1.3.3",
+ "@types/dockerode": "^3.3.47",
+ "@types/node": "^24.10.1",
+ "@types/react": "^19.2.7",
+ "@types/react-dom": "^19.2.3",
+ "drizzle-kit": "^0.31.7",
"lightningcss": "^1.30.2",
- "tailwindcss": "^4.1.14",
+ "tailwindcss": "^4.1.17",
"tinyglobby": "^0.2.15",
"tw-animate-css": "^1.4.0",
"typescript": "^5.9.3",
- "vite": "^7.1.9",
+ "vite": "^7.2.6",
"vite-bundle-analyzer": "^1.2.3",
"vite-tsconfig-paths": "^5.1.4"
}