From 8e24c6a8d8fd7677c938e5aa890c3f7e9e1e2c4c Mon Sep 17 00:00:00 2001 From: bcrooker Date: Sun, 1 Mar 2026 21:11:48 -0500 Subject: [PATCH 01/15] feat(backups): show estimated time remaining on backup progress --- app/client/api-client/types.gen.ts | 1 + .../modules/backups/components/backup-progress-card.tsx | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/app/client/api-client/types.gen.ts b/app/client/api-client/types.gen.ts index 4e107974..ad7ab3ea 100644 --- a/app/client/api-client/types.gen.ts +++ b/app/client/api-client/types.gen.ts @@ -4141,6 +4141,7 @@ export type GetBackupProgressResponses = { repositoryName: string; scheduleId: string; seconds_elapsed: number; + seconds_remaining: number; total_bytes: number; total_files: number; volumeName: string; diff --git a/app/client/modules/backups/components/backup-progress-card.tsx b/app/client/modules/backups/components/backup-progress-card.tsx index 053ac28d..4e9734b0 100644 --- a/app/client/modules/backups/components/backup-progress-card.tsx +++ b/app/client/modules/backups/components/backup-progress-card.tsx @@ -46,6 +46,9 @@ export const BackupProgressCard = ({ scheduleShortId, initialProgress }: Props) const currentFile = progress?.current_files?.[0] || ""; const fileName = currentFile.split("/").pop() || currentFile; const speed = progress ? formatBytes(progress.bytes_done / progress.seconds_elapsed) : null; + const eta = progress?.seconds_remaining != null && progress.seconds_remaining > 0 + ? formatDuration(progress.seconds_remaining) + : null; return ( @@ -96,6 +99,10 @@ export const BackupProgressCard = ({ scheduleShortId, initialProgress }: Props) {progress ? (progress.seconds_elapsed > 0 ? `${speed?.text} ${speed?.unit}/s` : "Calculating...") : "—"}

+
+

ETA

+

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

+
From 2adec5a9994b54cf04e8c26752cfe48d3b4d50ca Mon Sep 17 00:00:00 2001 From: bcrooker Date: Sat, 7 Mar 2026 11:22:41 -0500 Subject: [PATCH 02/15] fix: add seconds_remaining to restic backup progress schema --- app/schemas/restic-dto.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/app/schemas/restic-dto.ts b/app/schemas/restic-dto.ts index ba62338a..390cf265 100644 --- a/app/schemas/restic-dto.ts +++ b/app/schemas/restic-dto.ts @@ -37,6 +37,7 @@ export const resticBackupOutputSchema = resticBackupRunSummarySchema.and( export const resticBackupProgressMetricsSchema = type({ seconds_elapsed: "number", + seconds_remaining: "number = 0", percent_done: "number", total_files: "number", files_done: "number", From 7fde07f107acc0fc4dd47ea6d9a726a599891a03 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 7 Mar 2026 06:38:44 +0100 Subject: [PATCH 03/15] chore(deps): bump the minor-patch group across 1 directory with 13 updates (#629) * chore(deps): bump the minor-patch group across 1 directory with 13 updates Bumps the minor-patch group with 13 updates in the / directory: | Package | From | To | | --- | --- | --- | | [@better-auth/sso](https://github.com/better-auth/better-auth/tree/HEAD/packages/sso) | `1.5.2` | `1.5.4` | | [@scalar/hono-api-reference](https://github.com/scalar/scalar/tree/HEAD/integrations/hono) | `0.9.48` | `0.10.0` | | [@tanstack/react-hotkeys](https://github.com/TanStack/hotkeys/tree/HEAD/packages/react-hotkeys) | `0.3.1` | `0.3.2` | | [@tanstack/react-router](https://github.com/TanStack/router/tree/HEAD/packages/react-router) | `1.163.3` | `1.166.2` | | [@tanstack/react-router-ssr-query](https://github.com/TanStack/router/tree/HEAD/packages/react-router-ssr-query) | `1.163.3` | `1.166.2` | | [@tanstack/react-start](https://github.com/TanStack/router/tree/HEAD/packages/react-start) | `1.166.1` | `1.166.2` | | [arktype](https://github.com/arktypeio/arktype/tree/HEAD/ark/type) | `2.1.29` | `2.2.0` | | [better-auth](https://github.com/better-auth/better-auth/tree/HEAD/packages/better-auth) | `1.5.2` | `1.5.4` | | [es-toolkit](https://github.com/toss/es-toolkit) | `1.45.0` | `1.45.1` | | [hono](https://github.com/honojs/hono) | `4.12.4` | `4.12.5` | | [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `0.576.0` | `0.577.0` | | [@hey-api/openapi-ts](https://github.com/hey-api/openapi-ts) | `0.93.1` | `0.94.0` | | [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.3.3` | `25.3.5` | Updates `@better-auth/sso` from 1.5.2 to 1.5.4 - [Release notes](https://github.com/better-auth/better-auth/releases) - [Commits](https://github.com/better-auth/better-auth/commits/v1.5.4/packages/sso) Updates `@scalar/hono-api-reference` from 0.9.48 to 0.10.0 - [Release notes](https://github.com/scalar/scalar/releases) - [Changelog](https://github.com/scalar/scalar/blob/main/integrations/hono/CHANGELOG.md) - [Commits](https://github.com/scalar/scalar/commits/HEAD/integrations/hono) Updates `@tanstack/react-hotkeys` from 0.3.1 to 0.3.2 - [Release notes](https://github.com/TanStack/hotkeys/releases) - [Changelog](https://github.com/TanStack/hotkeys/blob/main/packages/react-hotkeys/CHANGELOG.md) - [Commits](https://github.com/TanStack/hotkeys/commits/@tanstack/react-hotkeys@0.3.2/packages/react-hotkeys) Updates `@tanstack/react-router` from 1.163.3 to 1.166.2 - [Release notes](https://github.com/TanStack/router/releases) - [Commits](https://github.com/TanStack/router/commits/v1.166.2/packages/react-router) Updates `@tanstack/react-router-ssr-query` from 1.163.3 to 1.166.2 - [Release notes](https://github.com/TanStack/router/releases) - [Commits](https://github.com/TanStack/router/commits/v1.166.2/packages/react-router-ssr-query) Updates `@tanstack/react-start` from 1.166.1 to 1.166.2 - [Release notes](https://github.com/TanStack/router/releases) - [Commits](https://github.com/TanStack/router/commits/v1.166.2/packages/react-start) Updates `arktype` from 2.1.29 to 2.2.0 - [Release notes](https://github.com/arktypeio/arktype/releases) - [Changelog](https://github.com/arktypeio/arktype/blob/main/ark/type/CHANGELOG.md) - [Commits](https://github.com/arktypeio/arktype/commits/arktype@2.2.0/ark/type) Updates `better-auth` from 1.5.2 to 1.5.4 - [Release notes](https://github.com/better-auth/better-auth/releases) - [Commits](https://github.com/better-auth/better-auth/commits/v1.5.4/packages/better-auth) Updates `es-toolkit` from 1.45.0 to 1.45.1 - [Release notes](https://github.com/toss/es-toolkit/releases) - [Changelog](https://github.com/toss/es-toolkit/blob/main/CHANGELOG.md) - [Commits](https://github.com/toss/es-toolkit/compare/v1.45.0...v1.45.1) Updates `hono` from 4.12.4 to 4.12.5 - [Release notes](https://github.com/honojs/hono/releases) - [Commits](https://github.com/honojs/hono/compare/v4.12.4...v4.12.5) Updates `lucide-react` from 0.576.0 to 0.577.0 - [Release notes](https://github.com/lucide-icons/lucide/releases) - [Commits](https://github.com/lucide-icons/lucide/commits/0.577.0/packages/lucide-react) Updates `@hey-api/openapi-ts` from 0.93.1 to 0.94.0 - [Release notes](https://github.com/hey-api/openapi-ts/releases) - [Changelog](https://github.com/hey-api/openapi-ts/blob/main/docs/CHANGELOG.md) - [Commits](https://github.com/hey-api/openapi-ts/compare/@hey-api/openapi-ts@0.93.1...@hey-api/openapi-ts@0.94.0) Updates `@types/node` from 25.3.3 to 25.3.5 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) --- updated-dependencies: - dependency-name: "@better-auth/sso" dependency-version: 1.5.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: minor-patch - dependency-name: "@scalar/hono-api-reference" dependency-version: 0.10.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: "@tanstack/react-hotkeys" dependency-version: 0.3.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: minor-patch - dependency-name: "@tanstack/react-router" dependency-version: 1.166.2 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: "@tanstack/react-router-ssr-query" dependency-version: 1.166.2 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: "@tanstack/react-start" dependency-version: 1.166.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: minor-patch - dependency-name: arktype dependency-version: 2.2.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: better-auth dependency-version: 1.5.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: minor-patch - dependency-name: es-toolkit dependency-version: 1.45.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: minor-patch - dependency-name: hono dependency-version: 4.12.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: minor-patch - dependency-name: lucide-react dependency-version: 0.577.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: "@hey-api/openapi-ts" dependency-version: 0.94.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: "@types/node" dependency-version: 25.3.5 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: minor-patch ... Signed-off-by: dependabot[bot] * chore: gen api-client --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Nicolas Meienberger --- app/client/api-client/types.gen.ts | 4 +- bun.lock | 110 ++++++++++++++++------------- package.json | 26 +++---- 3 files changed, 77 insertions(+), 63 deletions(-) diff --git a/app/client/api-client/types.gen.ts b/app/client/api-client/types.gen.ts index ad7ab3ea..1134d3f3 100644 --- a/app/client/api-client/types.gen.ts +++ b/app/client/api-client/types.gen.ts @@ -64,9 +64,9 @@ export type DeleteUserAccountData = { export type DeleteUserAccountErrors = { /** - * Forbidden + * Account not found */ - 403: unknown; + 404: unknown; /** * Cannot delete the last account */ diff --git a/bun.lock b/bun.lock index a1ddb948..bbd67fd0 100644 --- a/bun.lock +++ b/bun.lock @@ -5,7 +5,7 @@ "": { "name": "zerobyte", "dependencies": { - "@better-auth/sso": "^1.5.2", + "@better-auth/sso": "^1.5.4", "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", @@ -27,15 +27,15 @@ "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.8", - "@scalar/hono-api-reference": "^0.9.48", - "@tanstack/react-hotkeys": "^0.3.1", + "@scalar/hono-api-reference": "^0.10.0", + "@tanstack/react-hotkeys": "^0.3.2", "@tanstack/react-query": "^5.90.21", "@tanstack/react-query-devtools": "^5.91.3", - "@tanstack/react-router": "^1.163.3", - "@tanstack/react-router-ssr-query": "^1.163.3", - "@tanstack/react-start": "^1.166.1", - "arktype": "^2.1.28", - "better-auth": "^1.5.2", + "@tanstack/react-router": "^1.166.2", + "@tanstack/react-router-ssr-query": "^1.166.2", + "@tanstack/react-start": "^1.166.2", + "arktype": "^2.2.0", + "better-auth": "^1.5.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "commander": "^14.0.2", @@ -46,14 +46,14 @@ "dither-plugin": "^1.1.1", "dotenv": "^17.3.1", "drizzle-orm": "^1.0.0-beta.16-ea816b6", - "es-toolkit": "^1.45.0", - "hono": "^4.12.4", + "es-toolkit": "^1.45.1", + "hono": "^4.12.5", "hono-openapi": "^1.3.0", "hono-rate-limiter": "^0.5.3", "http-errors-enhanced": "^4.0.2", "input-otp": "^1.4.2", "isbot": "^5.1.35", - "lucide-react": "^0.576.0", + "lucide-react": "^0.577.0", "next-themes": "^0.4.6", "node-cron": "^4.2.1", "qrcode.react": "^4.2.0", @@ -75,7 +75,7 @@ "@babel/preset-typescript": "^7.28.5", "@faker-js/faker": "^10.3.0", "@happy-dom/global-registrator": "^20.8.3", - "@hey-api/openapi-ts": "^0.93.1", + "@hey-api/openapi-ts": "^0.94.0", "@libsql/client": "^0.17.0", "@playwright/test": "^1.58.2", "@tailwindcss/typography": "^0.5.19", @@ -85,7 +85,7 @@ "@total-typescript/shoehorn": "^0.1.2", "@types/bun": "^1.3.10", "@types/content-disposition": "^0.5.9", - "@types/node": "^25.3.3", + "@types/node": "^25.3.5", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@types/semver": "^7.7.1", @@ -218,19 +218,19 @@ "@better-auth/core": ["@better-auth/core@1.5.2", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "better-call": "1.3.2", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types"] }, "sha512-svaKRVN/p3+g++kljLEedHC+RgDlGsVr87tKiATr5xIE7xqLO1If906pMTNMfhF08N5r7pMbix/mRYdObuPKHA=="], - "@better-auth/drizzle-adapter": ["@better-auth/drizzle-adapter@1.5.2", "", { "peerDependencies": { "@better-auth/core": "1.5.2", "@better-auth/utils": "^0.3.0", "drizzle-orm": ">=0.41.0" } }, "sha512-29e7UCwqTriIuDdEr1xbSx4qGg6Ag3aTopzRavPyOCYJyzTwePw8iZ9zaJF1fsLmLeany7LW069NMDf6+3tz/w=="], + "@better-auth/drizzle-adapter": ["@better-auth/drizzle-adapter@1.5.4", "", { "peerDependencies": { "@better-auth/core": "1.5.4", "@better-auth/utils": "^0.3.0", "drizzle-orm": ">=0.41.0" } }, "sha512-4M4nMAWrDd3TmpV6dONkJjybBVKRZghe5Oj0NNyDEoXubxastQdO7Sb5B54I1rTx5yoMgsqaB+kbJnu/9UgjQg=="], - "@better-auth/kysely-adapter": ["@better-auth/kysely-adapter@1.5.2", "", { "peerDependencies": { "@better-auth/core": "1.5.2", "@better-auth/utils": "^0.3.0", "kysely": "^0.27.0 || ^0.28.0" } }, "sha512-PaP+KPJ6Cw0DxzuZLH6eR0oFhS5Iq/KjYvmZt76fCl5Q7Ys9Cct0kXVVlrD1bftiznTpV+UKMfwjcCWvtc1l4w=="], + "@better-auth/kysely-adapter": ["@better-auth/kysely-adapter@1.5.4", "", { "peerDependencies": { "@better-auth/core": "1.5.4", "@better-auth/utils": "^0.3.0", "kysely": "^0.27.0 || ^0.28.0" } }, "sha512-DPww7rIfz6Ed7dZlJSW9xMQ42VKaJLB5Cs+pPqd+UHKRyighKjf3VgvMIcAdFPc4olQ0qRHo3+ZJhFlBCxRhxA=="], - "@better-auth/memory-adapter": ["@better-auth/memory-adapter@1.5.2", "", { "peerDependencies": { "@better-auth/core": "1.5.2", "@better-auth/utils": "^0.3.0" } }, "sha512-vCbmMpUAisISRVxzNFIxkvO3wm63fzwSxAOO9Xktl77VyEyh8zliBNcW8S6+9DOeolVRKhjWEg7UaBOtIhFX/Q=="], + "@better-auth/memory-adapter": ["@better-auth/memory-adapter@1.5.4", "", { "peerDependencies": { "@better-auth/core": "1.5.4", "@better-auth/utils": "^0.3.0" } }, "sha512-iiWYut9rbQqiAsgRBtj6+nxanwjapxRgpIJbiS2o81h7b9iclE0AiDA0Foes590gdFQvskNauZcCpuF8ytxthg=="], - "@better-auth/mongo-adapter": ["@better-auth/mongo-adapter@1.5.2", "", { "peerDependencies": { "@better-auth/core": "1.5.2", "@better-auth/utils": "^0.3.0", "mongodb": "^6.0.0 || ^7.0.0" } }, "sha512-fJd+Z7fgRFgK2W7oxbFYM9K10yEUs/hlALzihCdDgSyRg9XFLBMJkMcECRqBsop/cZUjwZxLBTQPsIpgx9ix1g=="], + "@better-auth/mongo-adapter": ["@better-auth/mongo-adapter@1.5.4", "", { "peerDependencies": { "@better-auth/core": "1.5.4", "@better-auth/utils": "^0.3.0", "mongodb": "^6.0.0 || ^7.0.0" } }, "sha512-ArzJN5Obk6i6+vLK1HpPzLIcsjxZYXPPUvxVU8eyU5HyoUT2MlswWfPQ8UJAKPn0iq/T4PVp/wZcQMhWk1tuNA=="], - "@better-auth/prisma-adapter": ["@better-auth/prisma-adapter@1.5.2", "", { "dependencies": { "@prisma/client": "^7.4.1" }, "peerDependencies": { "@better-auth/core": "1.5.2", "@better-auth/utils": "^0.3.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-9GB4q91Q16wSSsi1vFP/RZNW0GNmdxTiDcE2MX1fC4Hlmo6UuT4WRv+94YPYs3/tPhsJ17k5Lt+rKTOK8S6Kbw=="], + "@better-auth/prisma-adapter": ["@better-auth/prisma-adapter@1.5.4", "", { "peerDependencies": { "@better-auth/core": "1.5.4", "@better-auth/utils": "^0.3.0", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-ZQTbcBopw/ezjjbNFsfR3CRp0QciC4tJCarAnB5G9fZtUYbDjfY0vZOxIRmU4kI3x755CXQpGqTrkwmXaMRa3w=="], - "@better-auth/sso": ["@better-auth/sso@1.5.2", "", { "dependencies": { "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21", "fast-xml-parser": "^5.4.1", "jose": "^6.1.3", "samlify": "^2.10.2", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/core": "1.5.2", "better-auth": "1.5.2", "better-call": "1.3.2" } }, "sha512-qPOkai2aBjJzwee3RuSYc17R5OPB9P1IONpKFHiAXBmp1RkUUwMwwN/QZC7PuJaecaZYSgKwRQ4+Ip4XYqYTeA=="], + "@better-auth/sso": ["@better-auth/sso@1.5.4", "", { "dependencies": { "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21", "fast-xml-parser": "^5.4.1", "jose": "^6.1.3", "samlify": "^2.10.2", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/core": "1.5.4", "better-auth": "1.5.4", "better-call": "1.3.2" } }, "sha512-XWrPIBp9d9Yx5bTe6lxQSkkRv14SQj0CFhblXM7cuCbK4ehk6xmBwiqcwJOMohGBrC6qxRItT0YkB9w1BCqP2A=="], - "@better-auth/telemetry": ["@better-auth/telemetry@1.5.2", "", { "dependencies": { "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21" }, "peerDependencies": { "@better-auth/core": "1.5.2" } }, "sha512-Cukyj5yciFNGtx5SkQCop2wOjpHuq2ZHL3V7ydVFe51WUNCXd+v1Sbed2sIfT/8hOcSB+NefALXNv1yTSxKkAQ=="], + "@better-auth/telemetry": ["@better-auth/telemetry@1.5.4", "", { "dependencies": { "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21" }, "peerDependencies": { "@better-auth/core": "1.5.4" } }, "sha512-mGXTY7Ecxo7uvlMr6TFCBUvlH0NUMOeE9LKgPhG4HyhBN6VfCEg/DD9PG0Z2IatmMWQbckkt7ox5A0eBpG9m5w=="], "@better-auth/utils": ["@better-auth/utils@0.3.1", "", {}, "sha512-+CGp4UmZSUrHHnpHhLPYu6cV+wSUSvVbZbNykxhUDocpVNTo9uFFxw/NqJlh1iC4wQ9HKKWGCKuZ5wUgS0v6Kg=="], @@ -330,13 +330,13 @@ "@happy-dom/global-registrator": ["@happy-dom/global-registrator@20.8.3", "", { "dependencies": { "@types/node": ">=20.0.0", "happy-dom": "^20.8.3" } }, "sha512-9z6lLr6K6dIFLiB9jI0tKTlYzCComn5RL7L1mN3EdMSCyRQB8I6A0FO/On8yYKkXuunexzPDC98zDflvv2wDrQ=="], - "@hey-api/codegen-core": ["@hey-api/codegen-core@0.7.0", "", { "dependencies": { "@hey-api/types": "0.1.3", "ansi-colors": "4.1.3", "c12": "3.3.3", "color-support": "1.1.3" }, "peerDependencies": { "typescript": ">=5.5.3" } }, "sha512-HglL4B4QwpzocE+c8qDU6XK8zMf8W8Pcv0RpFDYxHuYALWLTnpDUuEsglC7NQ4vC1maoXsBpMbmwpco0N4QviA=="], + "@hey-api/codegen-core": ["@hey-api/codegen-core@0.7.1", "", { "dependencies": { "@hey-api/types": "0.1.3", "ansi-colors": "4.1.3", "c12": "3.3.3", "color-support": "1.1.3" }, "peerDependencies": { "typescript": ">=5.5.3" } }, "sha512-X5qG+rr/BJvr+pEGcoW6l2azoZGrVuxsviEIhuf+3VwL9bk0atfubT65Xwo+4jDxXvjbhZvlwS0Ty3I7mLE2fg=="], "@hey-api/json-schema-ref-parser": ["@hey-api/json-schema-ref-parser@1.3.1", "", { "dependencies": { "@jsdevtools/ono": "7.1.3", "@types/json-schema": "7.0.15", "js-yaml": "4.1.1" } }, "sha512-7atnpUkT8TyUPHYPLk91j/GyaqMuwTEHanLOe50Dlx0EEvNuQqFD52Yjg8x4KU0UFL1mWlyhE+sUE/wAtQ1N2A=="], - "@hey-api/openapi-ts": ["@hey-api/openapi-ts@0.93.1", "", { "dependencies": { "@hey-api/codegen-core": "0.7.0", "@hey-api/json-schema-ref-parser": "1.3.1", "@hey-api/shared": "0.2.1", "@hey-api/types": "0.1.3", "ansi-colors": "4.1.3", "color-support": "1.1.3", "commander": "14.0.3" }, "peerDependencies": { "typescript": ">=5.5.3" }, "bin": { "openapi-ts": "bin/run.js" } }, "sha512-oQJPHiVkJKesZFpoW3jfQhrSQ7xdgzai7895ENl6ZDjCaIK6bOUTly7bsu+7+0ONsGH9jbtGbkoUzC+MtY+RKg=="], + "@hey-api/openapi-ts": ["@hey-api/openapi-ts@0.94.0", "", { "dependencies": { "@hey-api/codegen-core": "0.7.1", "@hey-api/json-schema-ref-parser": "1.3.1", "@hey-api/shared": "0.2.2", "@hey-api/types": "0.1.3", "ansi-colors": "4.1.3", "color-support": "1.1.3", "commander": "14.0.3" }, "peerDependencies": { "typescript": ">=5.5.3" }, "bin": { "openapi-ts": "bin/run.js" } }, "sha512-dbg3GG+v7sg9/Ahb7yFzwzQIJwm151JAtsnh9KtFyqiN0rGkMGA3/VqogEUq1kJB9XWrlMQwigwzhiEQ33VCSg=="], - "@hey-api/shared": ["@hey-api/shared@0.2.1", "", { "dependencies": { "@hey-api/codegen-core": "0.7.0", "@hey-api/json-schema-ref-parser": "1.3.1", "@hey-api/types": "0.1.3", "ansi-colors": "4.1.3", "cross-spawn": "7.0.6", "open": "11.0.0", "semver": "7.7.3" }, "peerDependencies": { "typescript": ">=5.5.3" } }, "sha512-uWI9047e9OVe3Ss+6vPMnRiixjRcjcBbdgpeq4IQymet3+wsn0+N/4RLDHBz1h57SemaxayPRUA0JOOsuC1qyA=="], + "@hey-api/shared": ["@hey-api/shared@0.2.2", "", { "dependencies": { "@hey-api/codegen-core": "0.7.1", "@hey-api/json-schema-ref-parser": "1.3.1", "@hey-api/types": "0.1.3", "ansi-colors": "4.1.3", "cross-spawn": "7.0.6", "open": "11.0.0", "semver": "7.7.3" }, "peerDependencies": { "typescript": ">=5.5.3" } }, "sha512-vMqCS+j7F9xpWoXC7TBbqZkaelwrdeuSB+s/3elu54V5iq++S59xhkSq5rOgDIpI1trpE59zZQa6dpyUxItOgw=="], "@hey-api/types": ["@hey-api/types@0.1.3", "", { "peerDependencies": { "typescript": ">=5.5.3" } }, "sha512-mZaiPOWH761yD4GjDQvtjS2ZYLu5o5pI1TVSvV/u7cmbybv51/FVtinFBeaE1kFQCKZ8OQpn2ezjLBJrKsGATw=="], @@ -770,13 +770,13 @@ "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="], - "@scalar/core": ["@scalar/core@0.3.45", "", { "dependencies": { "@scalar/types": "0.6.10" } }, "sha512-f3jyzColUUcu3eYfjslgNjG2JABuFU8WR3P7a9GevMpisW3skWUqVVffyC1P4w4i9TVRk9FUjYEtGeP10Rw9lQ=="], + "@scalar/core": ["@scalar/core@0.4.0", "", { "dependencies": { "@scalar/types": "0.7.0" } }, "sha512-Zcl+V8oBxb0S7vR+Nro8J53GD/w/kSjuyX0UoT3r1sn0bUM3Buf4Ob44n3CSfluQzxmiMuZxfOROHqa9F+ckbg=="], - "@scalar/helpers": ["@scalar/helpers@0.2.18", "", {}, "sha512-w1d4tpNEVZ293oB2BAgLrS0kVPUtG3eByNmOCJA5eK9vcT4D3cmsGtWjUaaqit0BQCsBFHK51rasGvSWnApYTw=="], + "@scalar/helpers": ["@scalar/helpers@0.3.0", "", {}, "sha512-lhQdehgighJC+PiSTJbbggM/SM3UydcRQil6Cfp/M4l539qklIh35pt4eh1+H+5Esa03gHnJwhTHF3TwglSOJw=="], - "@scalar/hono-api-reference": ["@scalar/hono-api-reference@0.9.48", "", { "dependencies": { "@scalar/core": "0.3.45" }, "peerDependencies": { "hono": "^4.11.5" } }, "sha512-SC7Qw39/RzHTajapzZ//5v2RbIePd8JzAVOk2sRaK3E4jR12OUk9m0ieAj+BByqdDdHtO6u6Jj07jgOxQlhQeA=="], + "@scalar/hono-api-reference": ["@scalar/hono-api-reference@0.10.0", "", { "dependencies": { "@scalar/core": "0.4.0" }, "peerDependencies": { "hono": "^4.11.5" } }, "sha512-5QGNilMAnLRzSufMhh0Ni8DepWzL2UOJm+RQI+e/slrhhfhO+pSV2bcLE8dhBz6k+V70El6Pvl0m2cPaDvP4sw=="], - "@scalar/types": ["@scalar/types@0.6.10", "", { "dependencies": { "@scalar/helpers": "0.2.18", "nanoid": "^5.1.6", "type-fest": "^5.3.1", "zod": "^4.3.5" } }, "sha512-fZkelRwcEeAhsn4c0wjYXWrzSzLaEyfxTn/eazXJ4XfCIsgJTQyK0FD8mnOBZJ2vEIbtT2E1mBKnCbDxrJIlxA=="], + "@scalar/types": ["@scalar/types@0.7.0", "", { "dependencies": { "@scalar/helpers": "0.3.0", "nanoid": "^5.1.6", "type-fest": "^5.3.1", "zod": "^4.3.5" } }, "sha512-IkG62M4ztmqkYNVhLpcswBojlQctbXLdkDa3UFsY8FfT7yfZ2LppjptycW9tWjD09ZQb4QAZ070FAUHmFRIS7w=="], "@standard-community/standard-json": ["@standard-community/standard-json@0.3.5", "", { "peerDependencies": { "@standard-schema/spec": "^1.0.0", "@types/json-schema": "^7.0.15", "@valibot/to-json-schema": "^1.3.0", "arktype": "^2.1.20", "effect": "^3.16.8", "quansync": "^0.2.11", "sury": "^10.0.0", "typebox": "^1.0.17", "valibot": "^1.1.0", "zod": "^3.25.0 || ^4.0.0", "zod-to-json-schema": "^3.24.5" }, "optionalPeers": ["@valibot/to-json-schema", "arktype", "effect", "sury", "typebox", "valibot", "zod", "zod-to-json-schema"] }, "sha512-4+ZPorwDRt47i+O7RjyuaxHRK/37QY/LmgxlGrRrSTLYoFatEOzvqIc85GTlM18SFZ5E91C+v0o/M37wZPpUHA=="], @@ -820,49 +820,49 @@ "@tanstack/history": ["@tanstack/history@1.161.4", "", {}, "sha512-Kp/WSt411ZWYvgXy6uiv5RmhHrz9cAml05AQPrtdAp7eUqvIDbMGPnML25OKbzR3RJ1q4wgENxDTvlGPa9+Mww=="], - "@tanstack/hotkeys": ["@tanstack/hotkeys@0.3.1", "", { "dependencies": { "@tanstack/store": "^0.9.1" } }, "sha512-G+v+PUac8ff/jg752yhhfPqw7/0xr8RYKL69Jb4i7L1JCPKwwoO4aLqVSmI17WSDN6sh1+nNf8QzUOphuPLAuw=="], + "@tanstack/hotkeys": ["@tanstack/hotkeys@0.3.2", "", { "dependencies": { "@tanstack/store": "^0.9.1" } }, "sha512-+ihZ/aaD2kvKLasLMdKL9bPASMruJhfVT052deIBMSVb5yuB1x82MpPuGsaKJy6A74fGOdmlxLcARBKXG1ZIaQ=="], "@tanstack/query-core": ["@tanstack/query-core@5.90.20", "", {}, "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg=="], "@tanstack/query-devtools": ["@tanstack/query-devtools@5.93.0", "", {}, "sha512-+kpsx1NQnOFTZsw6HAFCW3HkKg0+2cepGtAWXjiiSOJJ1CtQpt72EE2nyZb+AjAbLRPoeRmPJ8MtQd8r8gsPdg=="], - "@tanstack/react-hotkeys": ["@tanstack/react-hotkeys@0.3.1", "", { "dependencies": { "@tanstack/hotkeys": "0.3.1", "@tanstack/react-store": "^0.9.1" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-QXTIwVy4mdLZD0Fz501hzw85aNEI9522kLnxF85Bgyr6iKSHYq5L6ChBsBVx28d4zbyEiDQ6HHf96/qzIcNtOg=="], + "@tanstack/react-hotkeys": ["@tanstack/react-hotkeys@0.3.2", "", { "dependencies": { "@tanstack/hotkeys": "0.3.2", "@tanstack/react-store": "^0.9.1" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-tSYs4N65I87coXO6D4D24QkWdrQVsldRBxpPubr+gE7P1bU+a8j8B1Lv379j+TcCtpK1IzgJ88SCkqS9dyf49g=="], "@tanstack/react-query": ["@tanstack/react-query@5.90.21", "", { "dependencies": { "@tanstack/query-core": "5.90.20" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg=="], "@tanstack/react-query-devtools": ["@tanstack/react-query-devtools@5.91.3", "", { "dependencies": { "@tanstack/query-devtools": "5.93.0" }, "peerDependencies": { "@tanstack/react-query": "^5.90.20", "react": "^18 || ^19" } }, "sha512-nlahjMtd/J1h7IzOOfqeyDh5LNfG0eULwlltPEonYy0QL+nqrBB+nyzJfULV+moL7sZyxc2sHdNJki+vLA9BSA=="], - "@tanstack/react-router": ["@tanstack/react-router@1.163.3", "", { "dependencies": { "@tanstack/history": "1.161.4", "@tanstack/react-store": "^0.9.1", "@tanstack/router-core": "1.163.3", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-hheBbFVb+PbxtrWp8iy6+TTRTbhx3Pn6hKo8Tv/sWlG89ZMcD1xpQWzx8ukHN9K8YWbh5rdzt4kv6u8X4kB28Q=="], + "@tanstack/react-router": ["@tanstack/react-router@1.166.2", "", { "dependencies": { "@tanstack/history": "1.161.4", "@tanstack/react-store": "^0.9.1", "@tanstack/router-core": "1.166.2", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-pKhUtrvVLlhjWhsHkJSuIzh1J4LcP+8ErbIqRLORX9Js8dUFMKoT0+8oFpi+P8QRpuhm/7rzjYiWfcyTsqQZtA=="], - "@tanstack/react-router-ssr-query": ["@tanstack/react-router-ssr-query@1.163.3", "", { "dependencies": { "@tanstack/router-ssr-query-core": "1.163.3" }, "peerDependencies": { "@tanstack/query-core": ">=5.90.0", "@tanstack/react-query": ">=5.90.0", "@tanstack/react-router": ">=1.127.0", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-O7dPDbuSJIH7hNfpJxnaHuslciZEe97gP80A9leUto+5D/TlG8Fj1WXvbDkMm8PoWSK8UPdqp7vLg3a4XKc3qQ=="], + "@tanstack/react-router-ssr-query": ["@tanstack/react-router-ssr-query@1.166.2", "", { "dependencies": { "@tanstack/router-ssr-query-core": "1.166.2" }, "peerDependencies": { "@tanstack/query-core": ">=5.90.0", "@tanstack/react-query": ">=5.90.0", "@tanstack/react-router": ">=1.127.0", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-ERtz62+94ZF5kY35wlTN0pLL8hY2Enqn/AjauHaeHkvGh8WsN3dQdvPrPqI6urUWwzEjJjkC3fBDsim88HF8rQ=="], - "@tanstack/react-start": ["@tanstack/react-start@1.166.1", "", { "dependencies": { "@tanstack/react-router": "1.163.3", "@tanstack/react-start-client": "1.164.1", "@tanstack/react-start-server": "1.166.0", "@tanstack/router-utils": "^1.161.4", "@tanstack/start-client-core": "1.164.1", "@tanstack/start-plugin-core": "1.166.1", "@tanstack/start-server-core": "1.166.0", "pathe": "^2.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0", "vite": ">=7.0.0" } }, "sha512-hx/t39mz71GnrzRP/7sZKxvvOkuaNTo4oWsTlIGofVy19fTGhMsgOSTWPrdAY6c5OiFIPyIpmMFp1gpZbOkY+Q=="], + "@tanstack/react-start": ["@tanstack/react-start@1.166.2", "", { "dependencies": { "@tanstack/react-router": "1.166.2", "@tanstack/react-start-client": "1.166.2", "@tanstack/react-start-server": "1.166.2", "@tanstack/router-utils": "^1.161.4", "@tanstack/start-client-core": "1.166.2", "@tanstack/start-plugin-core": "1.166.2", "@tanstack/start-server-core": "1.166.2", "pathe": "^2.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0", "vite": ">=7.0.0" } }, "sha512-ryeDIITTVmGmOkTrdg4dL4Sl+LXK5w8BZtzLtsr3YxNhQaPwxqX4r69iuBt5M8jyXEsWwbJJdToN3xLr7CO5XQ=="], - "@tanstack/react-start-client": ["@tanstack/react-start-client@1.164.1", "", { "dependencies": { "@tanstack/react-router": "1.163.3", "@tanstack/router-core": "1.163.3", "@tanstack/start-client-core": "1.164.1", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-Ez3EN9/tl8Km0joBuKkuWLBwDWoe3o+x0REF0FE1cf/tfuV1aItQQ1adb5DAPlm7ys402NQDdHeuGnY3bWOnqg=="], + "@tanstack/react-start-client": ["@tanstack/react-start-client@1.166.2", "", { "dependencies": { "@tanstack/react-router": "1.166.2", "@tanstack/router-core": "1.166.2", "@tanstack/start-client-core": "1.166.2", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-Dlup62M5WV3LrLzLSiK8QTsYcSFKUlBffwP8evBzBAN/A4lDaTJat+EBqUxOYwR8gDVm1GR/H4O+7tg6ur2MBA=="], - "@tanstack/react-start-server": ["@tanstack/react-start-server@1.166.0", "", { "dependencies": { "@tanstack/history": "1.161.4", "@tanstack/react-router": "1.163.3", "@tanstack/router-core": "1.163.3", "@tanstack/start-client-core": "1.164.1", "@tanstack/start-server-core": "1.166.0" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-Knvga2rL5yWu9EgCtoFglTM0HZR3BNc6nBFOJUqBlLRobLmG0HggG2bD7/t4intO/YEjzYzfVT1YVug+vi7ang=="], + "@tanstack/react-start-server": ["@tanstack/react-start-server@1.166.2", "", { "dependencies": { "@tanstack/history": "1.161.4", "@tanstack/react-router": "1.166.2", "@tanstack/router-core": "1.166.2", "@tanstack/start-client-core": "1.166.2", "@tanstack/start-server-core": "1.166.2" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-0E7gp/8YFeCa3y65xI0ubyI8IzQjSiWn9D2Uo0r0Izi4/jQkv4Ee+niE4NqPAohY5oyxmcoLhnDqEyo0MqHIpQ=="], "@tanstack/react-store": ["@tanstack/react-store@0.9.1", "", { "dependencies": { "@tanstack/store": "0.9.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-YzJLnRvy5lIEFTLWBAZmcOjK3+2AepnBv/sr6NZmiqJvq7zTQggyK99Gw8fqYdMdHPQWXjz0epFKJXC+9V2xDA=="], - "@tanstack/router-core": ["@tanstack/router-core@1.163.3", "", { "dependencies": { "@tanstack/history": "1.161.4", "@tanstack/store": "^0.9.1", "cookie-es": "^2.0.0", "seroval": "^1.4.2", "seroval-plugins": "^1.4.2", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-jPptiGq/w3nuPzcMC7RNa79aU+b6OjaDzWJnBcV2UAwL4ThJamRS4h42TdhJE+oF5yH9IEnCOGQdfnbw45LbfA=="], + "@tanstack/router-core": ["@tanstack/router-core@1.166.2", "", { "dependencies": { "@tanstack/history": "1.161.4", "@tanstack/store": "^0.9.1", "cookie-es": "^2.0.0", "seroval": "^1.4.2", "seroval-plugins": "^1.4.2", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-zn3NhENOAX9ToQiX077UV2OH3aJKOvV2ZMNZZxZ3gDG3i3WqL8NfWfEgetEAfMN37/Mnt90PpotYgf7IyuoKqQ=="], - "@tanstack/router-generator": ["@tanstack/router-generator@1.164.0", "", { "dependencies": { "@tanstack/router-core": "1.163.3", "@tanstack/router-utils": "1.161.4", "@tanstack/virtual-file-routes": "1.161.4", "prettier": "^3.5.0", "recast": "^0.23.11", "source-map": "^0.7.4", "tsx": "^4.19.2", "zod": "^3.24.2" } }, "sha512-Uiyj+RtW0kdeqEd8NEd3Np1Z2nhJ2xgLS8U+5mTvFrm/s3xkM2LYjJHoLzc6am7sKPDsmeF9a4/NYq3R7ZJP0Q=="], + "@tanstack/router-generator": ["@tanstack/router-generator@1.166.2", "", { "dependencies": { "@tanstack/router-core": "1.166.2", "@tanstack/router-utils": "1.161.4", "@tanstack/virtual-file-routes": "1.161.4", "prettier": "^3.5.0", "recast": "^0.23.11", "source-map": "^0.7.4", "tsx": "^4.19.2", "zod": "^3.24.2" } }, "sha512-wbvdyP1PKKQKk4aVlGeK9S5uDy8zodTr3tEZ2gRKNavJLusXbEWqtoo42JxHFFNB6dtguehFMt8PyZPAtkgWwQ=="], - "@tanstack/router-plugin": ["@tanstack/router-plugin@1.164.0", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@tanstack/router-core": "1.163.3", "@tanstack/router-generator": "1.164.0", "@tanstack/router-utils": "1.161.4", "@tanstack/virtual-file-routes": "1.161.4", "chokidar": "^3.6.0", "unplugin": "^2.1.2", "zod": "^3.24.2" }, "peerDependencies": { "@rsbuild/core": ">=1.0.2", "@tanstack/react-router": "^1.163.3", "vite": ">=5.0.0 || >=6.0.0 || >=7.0.0", "vite-plugin-solid": "^2.11.10", "webpack": ">=5.92.0" }, "optionalPeers": ["@rsbuild/core", "@tanstack/react-router", "vite", "vite-plugin-solid", "webpack"] }, "sha512-cZPsEMhqzyzmuPuDbsTAzBZaT+cj0pGjwdhjxJfPCM06Ax8v4tFR7n/Ug0UCwnNAUEmKZWN3lA9uT+TxXnk9PQ=="], + "@tanstack/router-plugin": ["@tanstack/router-plugin@1.166.2", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@tanstack/router-core": "1.166.2", "@tanstack/router-generator": "1.166.2", "@tanstack/router-utils": "1.161.4", "@tanstack/virtual-file-routes": "1.161.4", "chokidar": "^3.6.0", "unplugin": "^2.1.2", "zod": "^3.24.2" }, "peerDependencies": { "@rsbuild/core": ">=1.0.2", "@tanstack/react-router": "^1.166.2", "vite": ">=5.0.0 || >=6.0.0 || >=7.0.0", "vite-plugin-solid": "^2.11.10", "webpack": ">=5.92.0" }, "optionalPeers": ["@rsbuild/core", "@tanstack/react-router", "vite", "vite-plugin-solid", "webpack"] }, "sha512-TnyV/7//Vp5fR49mmNbOWHGz9IJTm1lqVxzPdtpzg7D5PjkW2HFmLFLtWwpJgz2R7AJJWR4Ge5kIPmC+fVZ6eQ=="], - "@tanstack/router-ssr-query-core": ["@tanstack/router-ssr-query-core@1.163.3", "", { "peerDependencies": { "@tanstack/query-core": ">=5.90.0", "@tanstack/router-core": ">=1.127.0" } }, "sha512-i01cZdgG+fOEEuDclg1eq2X5Ptm0UF5cNYeFwMDBLQnNfil5NrDwxdFPdDx/YLevMaeGbVbiFS0m0u9A3ItzjA=="], + "@tanstack/router-ssr-query-core": ["@tanstack/router-ssr-query-core@1.166.2", "", { "peerDependencies": { "@tanstack/query-core": ">=5.90.0", "@tanstack/router-core": ">=1.127.0" } }, "sha512-syzrtCFtCX/TFK0sy/VqwPtRamg3TL/vr8KTbmksERnHzgRYNm+XynEc0BF9u0LZ/m3gkmY+4n2F4//PUjMG2w=="], "@tanstack/router-utils": ["@tanstack/router-utils@1.161.4", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/generator": "^7.28.5", "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "ansis": "^4.1.0", "babel-dead-code-elimination": "^1.0.12", "diff": "^8.0.2", "pathe": "^2.0.3", "tinyglobby": "^0.2.15" } }, "sha512-r8TpjyIZoqrXXaf2DDyjd44gjGBoyE+/oEaaH68yLI9ySPO1gUWmQENZ1MZnmBnpUGN24NOZxdjDLc8npK0SAw=="], - "@tanstack/start-client-core": ["@tanstack/start-client-core@1.164.1", "", { "dependencies": { "@tanstack/router-core": "1.163.3", "@tanstack/start-fn-stubs": "1.161.4", "@tanstack/start-storage-context": "1.163.3", "seroval": "^1.4.2", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-xo5YbDQu4vUtbnXa9RopuTPeK4FMdmIspPBQv4J5u7uYlf4LhkFHHoNBqxpnm0u2LX4c6ObscrG1bT8N0FDDHA=="], + "@tanstack/start-client-core": ["@tanstack/start-client-core@1.166.2", "", { "dependencies": { "@tanstack/router-core": "1.166.2", "@tanstack/start-fn-stubs": "1.161.4", "@tanstack/start-storage-context": "1.166.2", "seroval": "^1.4.2", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-weADfq6bBWgib7Tf7J+P5zF7LHfUm8UuMhijwTfYjaDQ5za04N4PQk5msw2+VchUnSL44aQUA0WeOmoEkG1KMQ=="], "@tanstack/start-fn-stubs": ["@tanstack/start-fn-stubs@1.161.4", "", {}, "sha512-b8s6iSQ+ny0P4lGK0n3DKaL6EI7SECG0/89svDeYieVw2+MaFOJVcQo3rU3BUvmuOcIkgkE5IhdzkmzPXH6yfA=="], - "@tanstack/start-plugin-core": ["@tanstack/start-plugin-core@1.166.1", "", { "dependencies": { "@babel/code-frame": "7.27.1", "@babel/core": "^7.28.5", "@babel/types": "^7.28.5", "@rolldown/pluginutils": "1.0.0-beta.40", "@tanstack/router-core": "1.163.3", "@tanstack/router-generator": "1.164.0", "@tanstack/router-plugin": "1.164.0", "@tanstack/router-utils": "1.161.4", "@tanstack/start-client-core": "1.164.1", "@tanstack/start-server-core": "1.166.0", "cheerio": "^1.0.0", "exsolve": "^1.0.7", "pathe": "^2.0.3", "picomatch": "^4.0.3", "source-map": "^0.7.6", "srvx": "^0.11.7", "tinyglobby": "^0.2.15", "ufo": "^1.5.4", "vitefu": "^1.1.1", "xmlbuilder2": "^4.0.3", "zod": "^3.24.2" }, "peerDependencies": { "vite": ">=7.0.0" } }, "sha512-GL6o0PmTncyJRiSUV6hFi3oYoYzuR+VqarDku5pJyvVmyKsQ3iIraCMXduuYNtVrzLA5d+ijXeSyX1j7uOWohA=="], + "@tanstack/start-plugin-core": ["@tanstack/start-plugin-core@1.166.2", "", { "dependencies": { "@babel/code-frame": "7.27.1", "@babel/core": "^7.28.5", "@babel/types": "^7.28.5", "@rolldown/pluginutils": "1.0.0-beta.40", "@tanstack/router-core": "1.166.2", "@tanstack/router-generator": "1.166.2", "@tanstack/router-plugin": "1.166.2", "@tanstack/router-utils": "1.161.4", "@tanstack/start-client-core": "1.166.2", "@tanstack/start-server-core": "1.166.2", "cheerio": "^1.0.0", "exsolve": "^1.0.7", "pathe": "^2.0.3", "picomatch": "^4.0.3", "source-map": "^0.7.6", "srvx": "^0.11.7", "tinyglobby": "^0.2.15", "ufo": "^1.5.4", "vitefu": "^1.1.1", "xmlbuilder2": "^4.0.3", "zod": "^3.24.2" }, "peerDependencies": { "vite": ">=7.0.0" } }, "sha512-geJE4Y/IQ3ZDRB/AahCoUuLliK4bYREcWVF1ViJfD1qCjsQlXMhDSruvswiMWMTxKfMuzX/XuIJWhmtndxymjQ=="], - "@tanstack/start-server-core": ["@tanstack/start-server-core@1.166.0", "", { "dependencies": { "@tanstack/history": "1.161.4", "@tanstack/router-core": "1.163.3", "@tanstack/start-client-core": "1.164.1", "@tanstack/start-storage-context": "1.163.3", "h3-v2": "npm:h3@2.0.1-rc.14", "seroval": "^1.4.2", "tiny-invariant": "^1.3.3" } }, "sha512-DW/CgeqwQSEuNyjQo6MFoPfxv7uKlRsbRpiPzE2R0PH9GBF6c0M+R6ejD7VEqVFIURQJd8fPcvTfVjSWqJZYiA=="], + "@tanstack/start-server-core": ["@tanstack/start-server-core@1.166.2", "", { "dependencies": { "@tanstack/history": "1.161.4", "@tanstack/router-core": "1.166.2", "@tanstack/start-client-core": "1.166.2", "@tanstack/start-storage-context": "1.166.2", "h3-v2": "npm:h3@2.0.1-rc.14", "seroval": "^1.4.2", "tiny-invariant": "^1.3.3" } }, "sha512-9BDZsaLyHVux5tJRRBRYa2xW2jUaKr4PbJkTCSSOnAByOGUVJy7N+790/Q1Kq/LVud+0h42vZHWSRDDywfnedQ=="], - "@tanstack/start-storage-context": ["@tanstack/start-storage-context@1.163.3", "", { "dependencies": { "@tanstack/router-core": "1.163.3" } }, "sha512-JJfY4Ikz9yg9gye+PCsMoUO765UBjiFmwLywUjr5W3hJ5uUImdOGClh5ovuxw5pkVaWhw5r7YDRlmAyOkZOCmA=="], + "@tanstack/start-storage-context": ["@tanstack/start-storage-context@1.166.2", "", { "dependencies": { "@tanstack/router-core": "1.166.2" } }, "sha512-c3QPApFAhiDXDZ/zLvop5InErqCrawWuO751FpItGnelOlpOAPMw5/h//1u/RnIcOv2l/ffDBCbp+N09eFPhaA=="], "@tanstack/store": ["@tanstack/store@0.9.1", "", {}, "sha512-+qcNkOy0N1qSGsP7omVCW0SDrXtaDcycPqBDE726yryiA5eTDFpjBReaYjghVJwNf1pcPMyzIwTGlYjCSQR0Fg=="], @@ -926,7 +926,7 @@ "@types/mssql": ["@types/mssql@9.1.9", "", { "dependencies": { "@types/node": "*", "tarn": "^3.0.1", "tedious": "*" } }, "sha512-P0nCgw6vzY23UxZMnbI4N7fnLGANt4LI4yvxze1paPj+LuN28cFv5EI+QidP8udnId/BKhkcRhm/BleNsjK65A=="], - "@types/node": ["@types/node@25.3.3", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ=="], + "@types/node": ["@types/node@25.3.5", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA=="], "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], @@ -982,7 +982,7 @@ "arkregex": ["arkregex@0.0.5", "", { "dependencies": { "@ark/util": "0.56.0" } }, "sha512-ncYjBdLlh5/QnVsAA8De16Tc9EqmYM7y/WU9j+236KcyYNUXogpz3sC4ATIZYzzLxwI+0sEOaQLEmLmRleaEXw=="], - "arktype": ["arktype@2.1.29", "", { "dependencies": { "@ark/schema": "0.56.0", "@ark/util": "0.56.0", "arkregex": "0.0.5" } }, "sha512-jyfKk4xIOzvYNayqnD8ZJQqOwcrTOUbIU4293yrzAjA3O1dWh61j71ArMQ6tS/u4pD7vabSPe7nG3RCyoXW6RQ=="], + "arktype": ["arktype@2.2.0", "", { "dependencies": { "@ark/schema": "0.56.0", "@ark/util": "0.56.0", "arkregex": "0.0.5" } }, "sha512-t54MZ7ti5BhOEvzEkgKnWvqj+UbDfWig+DHr5I34xatymPusKLS0lQpNJd8M6DzmIto2QGszHfNKoFIT8tMCZQ=="], "asn1": ["asn1@0.2.6", "", { "dependencies": { "safer-buffer": "~2.1.0" } }, "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ=="], @@ -1000,7 +1000,7 @@ "baseline-browser-mapping": ["baseline-browser-mapping@2.10.0", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA=="], - "better-auth": ["better-auth@1.5.2", "", { "dependencies": { "@better-auth/core": "1.5.2", "@better-auth/drizzle-adapter": "1.5.2", "@better-auth/kysely-adapter": "1.5.2", "@better-auth/memory-adapter": "1.5.2", "@better-auth/mongo-adapter": "1.5.2", "@better-auth/prisma-adapter": "1.5.2", "@better-auth/telemetry": "1.5.2", "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.1.1", "@noble/hashes": "^2.0.1", "better-call": "1.3.2", "defu": "^6.1.4", "jose": "^6.1.3", "kysely": "^0.28.11", "nanostores": "^1.1.1", "zod": "^4.3.6" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": ">=0.41.0", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-xEnt/RBsu/cjcN+IEsgeBqka/6QRaGzPFl5hnnJfZU/JxRGnDsVUDHXvJQRfecj8qA/1nPJOQjZ6qQL1Z7aN+Q=="], + "better-auth": ["better-auth@1.5.4", "", { "dependencies": { "@better-auth/core": "1.5.4", "@better-auth/drizzle-adapter": "1.5.4", "@better-auth/kysely-adapter": "1.5.4", "@better-auth/memory-adapter": "1.5.4", "@better-auth/mongo-adapter": "1.5.4", "@better-auth/prisma-adapter": "1.5.4", "@better-auth/telemetry": "1.5.4", "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.1.1", "@noble/hashes": "^2.0.1", "better-call": "1.3.2", "defu": "^6.1.4", "jose": "^6.1.3", "kysely": "^0.28.11", "nanostores": "^1.1.1", "zod": "^4.3.6" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": ">=0.41.0", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-ReykcEKx6Kp9560jG1wtlDBnftA7L7xb3ZZdDWm5yGXKKe2pUf+oBjH0fqekrkRII0m4XBVQbQ0mOrFv+3FdYg=="], "better-call": ["better-call@1.3.2", "", { "dependencies": { "@better-auth/utils": "^0.3.1", "@better-fetch/fetch": "^1.1.21", "rou3": "^0.7.12", "set-cookie-parser": "^3.0.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-4cZIfrerDsNTn3cm+MhLbUePN0gdwkhSXEuG7r/zuQ8c/H7iU0/jSK5TD3FW7U0MgKHce/8jGpPYNO4Ve+4NBw=="], @@ -1184,7 +1184,7 @@ "entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], - "es-toolkit": ["es-toolkit@1.45.0", "", {}, "sha512-RArCX+Zea16+R1jg4mH223Z8p/ivbJjIkU3oC6ld2bdUfmDxiCkFYSi9zLOR2anucWJUeH4Djnzgd0im0nD3dw=="], + "es-toolkit": ["es-toolkit@1.45.1", "", {}, "sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw=="], "esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], @@ -1264,7 +1264,7 @@ "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="], - "hono": ["hono@4.12.4", "", {}, "sha512-ooiZW1Xy8rQ4oELQ++otI2T9DsKpV0M6c6cO6JGx4RTfav9poFFLlet9UMXHZnoM1yG0HWGlQLswBGX3RZmHtg=="], + "hono": ["hono@4.12.5", "", {}, "sha512-3qq+FUBtlTHhtYxbxheZgY8NIFnkkC/MR8u5TTsr7YZ3wixryQ3cCwn3iZbg8p8B88iDBBAYSfZDS75t8MN7Vg=="], "hono-openapi": ["hono-openapi@1.3.0", "", { "peerDependencies": { "@hono/standard-validator": "^0.2.0", "@standard-community/standard-json": "^0.3.5", "@standard-community/standard-openapi": "^0.2.9", "@types/json-schema": "^7.0.15", "hono": "^4.8.3", "openapi-types": "^12.1.3" }, "optionalPeers": ["@hono/standard-validator", "hono"] }, "sha512-xDvCWpWEIv0weEmnl3EjRQzqbHIO8LnfzMuYOCmbuyE5aes6aXxLg4vM3ybnoZD5TiTUkA6PuRQPJs3R7WRBig=="], @@ -1428,7 +1428,7 @@ "lru.min": ["lru.min@1.1.4", "", {}, "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA=="], - "lucide-react": ["lucide-react@0.576.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-koNxU14BXrxUfZQ9cUaP0ES1uyPZKYDjk31FQZB6dQ/x+tXk979sVAn9ppZ/pVeJJyOxVM8j1E+8QEuSc02Vug=="], + "lucide-react": ["lucide-react@0.577.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A=="], "luxon": ["luxon@3.7.2", "", {}, "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew=="], @@ -1908,6 +1908,18 @@ "@babel/helper-create-class-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@better-auth/drizzle-adapter/@better-auth/core": ["@better-auth/core@1.5.4", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "better-call": "1.3.2", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types"] }, "sha512-k5AdwPRQETZn0vdB60EB9CDxxfllpJXKqVxTjyXIUSRz7delNGlU0cR/iRP3VfVJwvYR1NbekphBDNo+KGoEzQ=="], + + "@better-auth/kysely-adapter/@better-auth/core": ["@better-auth/core@1.5.4", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "better-call": "1.3.2", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types"] }, "sha512-k5AdwPRQETZn0vdB60EB9CDxxfllpJXKqVxTjyXIUSRz7delNGlU0cR/iRP3VfVJwvYR1NbekphBDNo+KGoEzQ=="], + + "@better-auth/memory-adapter/@better-auth/core": ["@better-auth/core@1.5.4", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "better-call": "1.3.2", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types"] }, "sha512-k5AdwPRQETZn0vdB60EB9CDxxfllpJXKqVxTjyXIUSRz7delNGlU0cR/iRP3VfVJwvYR1NbekphBDNo+KGoEzQ=="], + + "@better-auth/mongo-adapter/@better-auth/core": ["@better-auth/core@1.5.4", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "better-call": "1.3.2", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types"] }, "sha512-k5AdwPRQETZn0vdB60EB9CDxxfllpJXKqVxTjyXIUSRz7delNGlU0cR/iRP3VfVJwvYR1NbekphBDNo+KGoEzQ=="], + + "@better-auth/prisma-adapter/@better-auth/core": ["@better-auth/core@1.5.4", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "better-call": "1.3.2", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types"] }, "sha512-k5AdwPRQETZn0vdB60EB9CDxxfllpJXKqVxTjyXIUSRz7delNGlU0cR/iRP3VfVJwvYR1NbekphBDNo+KGoEzQ=="], + + "@better-auth/telemetry/@better-auth/core": ["@better-auth/core@1.5.4", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "better-call": "1.3.2", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types"] }, "sha512-k5AdwPRQETZn0vdB60EB9CDxxfllpJXKqVxTjyXIUSRz7delNGlU0cR/iRP3VfVJwvYR1NbekphBDNo+KGoEzQ=="], + "@happy-dom/global-registrator/@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="], "@hey-api/shared/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], @@ -1982,6 +1994,8 @@ "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "better-auth/@better-auth/core": ["@better-auth/core@1.5.4", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "better-call": "1.3.2", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types"] }, "sha512-k5AdwPRQETZn0vdB60EB9CDxxfllpJXKqVxTjyXIUSRz7delNGlU0cR/iRP3VfVJwvYR1NbekphBDNo+KGoEzQ=="], + "bun-types/@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="], "cheerio/whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="], diff --git a/package.json b/package.json index 2c5c1aca..f621f47d 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "test:codegen": "playwright codegen localhost:4096" }, "dependencies": { - "@better-auth/sso": "^1.5.2", + "@better-auth/sso": "^1.5.4", "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", @@ -48,15 +48,15 @@ "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.8", - "@scalar/hono-api-reference": "^0.9.48", - "@tanstack/react-hotkeys": "^0.3.1", + "@scalar/hono-api-reference": "^0.10.0", + "@tanstack/react-hotkeys": "^0.3.2", "@tanstack/react-query": "^5.90.21", "@tanstack/react-query-devtools": "^5.91.3", - "@tanstack/react-router": "^1.163.3", - "@tanstack/react-router-ssr-query": "^1.163.3", - "@tanstack/react-start": "^1.166.1", - "arktype": "^2.1.28", - "better-auth": "^1.5.2", + "@tanstack/react-router": "^1.166.2", + "@tanstack/react-router-ssr-query": "^1.166.2", + "@tanstack/react-start": "^1.166.2", + "arktype": "^2.2.0", + "better-auth": "^1.5.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "commander": "^14.0.2", @@ -67,14 +67,14 @@ "dither-plugin": "^1.1.1", "dotenv": "^17.3.1", "drizzle-orm": "^1.0.0-beta.16-ea816b6", - "es-toolkit": "^1.45.0", - "hono": "^4.12.4", + "es-toolkit": "^1.45.1", + "hono": "^4.12.5", "hono-openapi": "^1.3.0", "hono-rate-limiter": "^0.5.3", "http-errors-enhanced": "^4.0.2", "input-otp": "^1.4.2", "isbot": "^5.1.35", - "lucide-react": "^0.576.0", + "lucide-react": "^0.577.0", "next-themes": "^0.4.6", "node-cron": "^4.2.1", "qrcode.react": "^4.2.0", @@ -96,7 +96,7 @@ "@babel/preset-typescript": "^7.28.5", "@faker-js/faker": "^10.3.0", "@happy-dom/global-registrator": "^20.8.3", - "@hey-api/openapi-ts": "^0.93.1", + "@hey-api/openapi-ts": "^0.94.0", "@libsql/client": "^0.17.0", "@playwright/test": "^1.58.2", "@tailwindcss/typography": "^0.5.19", @@ -106,7 +106,7 @@ "@total-typescript/shoehorn": "^0.1.2", "@types/bun": "^1.3.10", "@types/content-disposition": "^0.5.9", - "@types/node": "^25.3.3", + "@types/node": "^25.3.5", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@types/semver": "^7.7.1", From 3fa793bef6de92cdabef87baf2b60c8fabfd8116 Mon Sep 17 00:00:00 2001 From: Vutsal Date: Sat, 7 Mar 2026 21:51:28 -0800 Subject: [PATCH 04/15] Refactor scheduler to use CronExpressionParser --- app/server/core/scheduler.ts | 71 +++++++++++++++++++++++++++++++++++- 1 file changed, 69 insertions(+), 2 deletions(-) diff --git a/app/server/core/scheduler.ts b/app/server/core/scheduler.ts index d115fbeb..5a45b526 100644 --- a/app/server/core/scheduler.ts +++ b/app/server/core/scheduler.ts @@ -1,4 +1,4 @@ -import cron, { type ScheduledTask } from "node-cron"; +import CronExpressionParser from "cron-parser"; import { logger } from "../utils/logger"; export abstract class Job { @@ -7,6 +7,73 @@ export abstract class Job { type JobConstructor = new () => Job; +class ScheduledTask { + private timer: ReturnType | null = null; + private active = true; + private running = false; + + constructor( + private readonly jobName: string, + private readonly cronExpression: string, + private readonly run: () => Promise, + ) { + this.scheduleNext(); + } + + private getDelay(fromDate: Date) { + try { + const interval = CronExpressionParser.parse(this.cronExpression, { + currentDate: fromDate, + tz: Intl.DateTimeFormat().resolvedOptions().timeZone, + }); + const nextRun = interval.next().toDate(); + return Math.max(0, nextRun.getTime() - Date.now()); + } catch (error) { + logger.error(`Failed to parse cron expression for ${this.jobName}:`, error); + return 60_000; + } + } + + private scheduleNext(fromDate = new Date()) { + if (!this.active) return; + + const delay = this.getDelay(fromDate); + this.timer = setTimeout(() => { + void this.tick(); + }, delay); + } + + private async tick() { + if (!this.active) return; + + if (this.running) { + logger.warn(`Skipping overlapping run for job ${this.jobName}`); + this.scheduleNext(new Date()); + return; + } + + this.running = true; + try { + await this.run(); + } finally { + this.running = false; + this.scheduleNext(new Date()); + } + } + + async stop() { + this.active = false; + if (this.timer) { + clearTimeout(this.timer); + this.timer = null; + } + } + + async destroy() { + await this.stop(); + } +} + class SchedulerClass { private tasks: ScheduledTask[] = []; @@ -18,7 +85,7 @@ class SchedulerClass { const job = new JobClass(); return { schedule: (cronExpression: string) => { - const task = cron.schedule(cronExpression, async () => { + const task = new ScheduledTask(JobClass.name, cronExpression, async () => { try { await job.run(); } catch (error) { From d640ad88b2a24d959677483c05d86157f046a493 Mon Sep 17 00:00:00 2001 From: Nicolas Meienberger Date: Sun, 8 Mar 2026 08:54:23 +0100 Subject: [PATCH 05/15] refactor: harden cron validation and fail early in case of wrong pattern --- app/server/core/__tests__/scheduler.test.ts | 145 ++++++++++++++++++ app/server/core/scheduler.ts | 45 ++++-- app/server/modules/backups/backups.service.ts | 15 +- bun.lock | 3 - package.json | 1 - 5 files changed, 186 insertions(+), 23 deletions(-) create mode 100644 app/server/core/__tests__/scheduler.test.ts diff --git a/app/server/core/__tests__/scheduler.test.ts b/app/server/core/__tests__/scheduler.test.ts new file mode 100644 index 00000000..9d63869e --- /dev/null +++ b/app/server/core/__tests__/scheduler.test.ts @@ -0,0 +1,145 @@ +import { afterEach, describe, expect, mock, spyOn, test, vi } from "bun:test"; +import { logger } from "~/server/utils/logger"; +import { Job, Scheduler } from "../scheduler"; + +const flushMicrotasks = async () => { + await Promise.resolve(); +}; + +const mockTimeZone = (timeZone: string) => { + const resolvedOptions = Intl.DateTimeFormat.prototype.resolvedOptions; + return spyOn(Intl.DateTimeFormat.prototype, "resolvedOptions").mockImplementation( + function (this: Intl.DateTimeFormat) { + return { ...resolvedOptions.call(this), timeZone }; + }, + ); +}; + +describe("Scheduler", () => { + afterEach(async () => { + await Scheduler.clear(); + mock.restore(); + vi.useRealTimers(); + }); + + test("keeps future cron ticks armed while a job is still running", async () => { + vi.useFakeTimers({ now: new Date("2026-03-08T00:00:00.000Z") }); + + let runCount = 0; + let releaseRun: (() => void) | undefined; + const warn = spyOn(logger, "warn"); + + class TestJob extends Job { + async run() { + runCount += 1; + await new Promise((resolve) => { + releaseRun = resolve; + }); + } + } + + Scheduler.build(TestJob).schedule("* * * * *"); + + expect(vi.getTimerCount()).toBe(1); + + vi.advanceTimersByTime(60_000); + await flushMicrotasks(); + + expect(runCount).toBe(1); + expect(vi.getTimerCount()).toBe(1); + + vi.advanceTimersByTime(60_000); + await flushMicrotasks(); + + expect(runCount).toBe(1); + expect(warn).toHaveBeenCalledWith("Skipping overlapping run for job TestJob"); + expect(vi.getTimerCount()).toBe(1); + + releaseRun?.(); + await flushMicrotasks(); + }); + + test("throws for invalid cron expressions instead of retrying with a fallback timer", () => { + vi.useFakeTimers({ now: new Date("2026-03-08T00:00:00.000Z") }); + + class InvalidCronJob extends Job { + async run() { + return; + } + } + + expect(() => Scheduler.build(InvalidCronJob).schedule("not a cron")).toThrow(); + expect(vi.getTimerCount()).toBe(0); + }); + + test("uses the local timezone instead of UTC when arming the next run", async () => { + mockTimeZone("America/New_York"); + vi.useFakeTimers({ now: new Date("2026-01-15T04:59:00.000Z") }); + + let runCount = 0; + + class MidnightJob extends Job { + async run() { + runCount += 1; + } + } + + Scheduler.build(MidnightJob).schedule("0 0 * * *"); + + vi.advanceTimersByTime(59_000); + await flushMicrotasks(); + expect(runCount).toBe(0); + + vi.advanceTimersByTime(1_000); + await flushMicrotasks(); + expect(runCount).toBe(1); + }); + + test("runs skipped-hour cron expressions once when daylight saving time jumps forward", async () => { + mockTimeZone("America/New_York"); + vi.useFakeTimers({ now: new Date("2026-03-08T06:59:00.000Z") }); + + let runCount = 0; + + class SpringForwardJob extends Job { + async run() { + runCount += 1; + } + } + + Scheduler.build(SpringForwardJob).schedule("0 2 * * *"); + + vi.advanceTimersByTime(60_000); + await flushMicrotasks(); + expect(runCount).toBe(1); + expect(vi.getTimerCount()).toBe(1); + + vi.advanceTimersByTime(60_000); + await flushMicrotasks(); + expect(runCount).toBe(1); + }); + + test("does not run the same repeated local time twice when daylight saving time falls back", async () => { + mockTimeZone("America/New_York"); + vi.useFakeTimers({ now: new Date("2026-11-01T05:20:00.000Z") }); + + let runCount = 0; + + class FallBackJob extends Job { + async run() { + runCount += 1; + } + } + + Scheduler.build(FallBackJob).schedule("30 1 * * *"); + + vi.advanceTimersByTime(10 * 60_000); + await flushMicrotasks(); + expect(runCount).toBe(1); + + vi.advanceTimersByTime(70 * 60_000); + await flushMicrotasks(); + expect(runCount).toBe(1); + expect(vi.getTimerCount()).toBe(1); + }); +}); diff --git a/app/server/core/scheduler.ts b/app/server/core/scheduler.ts index 5a45b526..ea6e9eff 100644 --- a/app/server/core/scheduler.ts +++ b/app/server/core/scheduler.ts @@ -11,29 +11,39 @@ class ScheduledTask { private timer: ReturnType | null = null; private active = true; private running = false; + private readonly timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone || undefined; constructor( private readonly jobName: string, private readonly cronExpression: string, private readonly run: () => Promise, ) { + CronExpressionParser.parse(this.cronExpression); this.scheduleNext(); } private getDelay(fromDate: Date) { - try { - const interval = CronExpressionParser.parse(this.cronExpression, { - currentDate: fromDate, - tz: Intl.DateTimeFormat().resolvedOptions().timeZone, - }); - const nextRun = interval.next().toDate(); - return Math.max(0, nextRun.getTime() - Date.now()); - } catch (error) { - logger.error(`Failed to parse cron expression for ${this.jobName}:`, error); - return 60_000; + const interval = CronExpressionParser.parse(this.cronExpression, { + currentDate: fromDate, + tz: this.timeZone, + }); + const nextRun = interval.next().toDate(); + return Math.max(0, nextRun.getTime() - Date.now()); + } + + private clearTimer() { + if (this.timer) { + clearTimeout(this.timer); + this.timer = null; } } + private stopScheduling(error: unknown) { + this.active = false; + this.clearTimer(); + logger.error(`Stopping scheduled job ${this.jobName} after cron parsing failed:`, error); + } + private scheduleNext(fromDate = new Date()) { if (!this.active) return; @@ -45,10 +55,17 @@ class ScheduledTask { private async tick() { if (!this.active) return; + this.timer = null; + + try { + this.scheduleNext(new Date()); + } catch (error) { + this.stopScheduling(error); + return; + } if (this.running) { logger.warn(`Skipping overlapping run for job ${this.jobName}`); - this.scheduleNext(new Date()); return; } @@ -57,16 +74,12 @@ class ScheduledTask { await this.run(); } finally { this.running = false; - this.scheduleNext(new Date()); } } async stop() { this.active = false; - if (this.timer) { - clearTimeout(this.timer); - this.timer = null; - } + this.clearTimer(); } async destroy() { diff --git a/app/server/modules/backups/backups.service.ts b/app/server/modules/backups/backups.service.ts index 8c65588c..52c6a8bb 100644 --- a/app/server/modules/backups/backups.service.ts +++ b/app/server/modules/backups/backups.service.ts @@ -1,6 +1,15 @@ import { and, eq, inArray } from "drizzle-orm"; -import cron from "node-cron"; +import CronExpressionParser from "cron-parser"; import { NotFoundError, BadRequestError, ConflictError } from "http-errors-enhanced"; + +const isValidCron = (expression: string): boolean => { + try { + CronExpressionParser.parse(expression); + return true; + } catch { + return false; + } +}; import { db } from "../../db/db"; import { backupScheduleMirrorsTable, backupScheduleNotificationsTable, backupSchedulesTable } from "../../db/schema"; import type { CreateBackupScheduleBody, UpdateBackupScheduleBody, UpdateScheduleMirrorsBody } from "./backups.dto"; @@ -83,7 +92,7 @@ const getScheduleByIdOrShortId = async (idOrShortId: string | number) => { const createSchedule = async (data: CreateBackupScheduleBody) => { const organizationId = getOrganizationId(); - if (!cron.validate(data.cronExpression)) { + if (!isValidCron(data.cronExpression)) { throw new BadRequestError("Invalid cron expression"); } @@ -169,7 +178,7 @@ const updateSchedule = async (scheduleIdOrShortId: number | string, data: Update throw new NotFoundError("Backup schedule not found"); } - if (data.cronExpression && !cron.validate(data.cronExpression)) { + if (data.cronExpression && !isValidCron(data.cronExpression)) { throw new BadRequestError("Invalid cron expression"); } diff --git a/bun.lock b/bun.lock index bbd67fd0..4a5d9b81 100644 --- a/bun.lock +++ b/bun.lock @@ -55,7 +55,6 @@ "isbot": "^5.1.35", "lucide-react": "^0.577.0", "next-themes": "^0.4.6", - "node-cron": "^4.2.1", "qrcode.react": "^4.2.0", "react": "^19.2.4", "react-dom": "^19.2.4", @@ -1554,8 +1553,6 @@ "nitro": ["nitro@3.0.1-alpha.2", "", { "dependencies": { "consola": "^3.4.2", "crossws": "^0.4.3", "db0": "^0.3.4", "h3": "^2.0.1-rc.11", "jiti": "^2.6.1", "nf3": "^0.3.5", "ofetch": "^2.0.0-alpha.3", "ohash": "^2.0.11", "oxc-minify": "^0.110.0", "oxc-transform": "^0.110.0", "srvx": "^0.10.1", "undici": "^7.18.2", "unenv": "^2.0.0-rc.24", "unstorage": "^2.0.0-alpha.5" }, "peerDependencies": { "rolldown": ">=1.0.0-beta.0", "rollup": "^4", "vite": "^7 || ^8 || >=8.0.0-0", "xml2js": "^0.6.2" }, "optionalPeers": ["rolldown", "rollup", "vite", "xml2js"], "bin": { "nitro": "dist/cli/index.mjs" } }, "sha512-YviDY5J/trS821qQ1fpJtpXWIdPYiOizC/meHavlm1Hfuhx//H+Egd1+4C5SegJRgtWMnRPW9n//6Woaw81cTQ=="], - "node-cron": ["node-cron@4.2.1", "", {}, "sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg=="], - "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], "node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], diff --git a/package.json b/package.json index f621f47d..b0600f0a 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,6 @@ "isbot": "^5.1.35", "lucide-react": "^0.577.0", "next-themes": "^0.4.6", - "node-cron": "^4.2.1", "qrcode.react": "^4.2.0", "react": "^19.2.4", "react-dom": "^19.2.4", From 1b948ed41e3a470915543c9302d36aab1e7339f2 Mon Sep 17 00:00:00 2001 From: Nico <47644445+nicotsx@users.noreply.github.com> Date: Sun, 8 Mar 2026 13:38:00 +0100 Subject: [PATCH 06/15] fix: single item file restore (#634) fix: single item file restore test(e2e): restore single file at custom location Closes #633 --- app/client/api-client/types.gen.ts | 1 + app/client/components/restore-form.tsx | 2 + .../modules/repositories/repositories.dto.ts | 1 + .../restic/commands/__tests__/restore.test.ts | 14 +++++ app/server/utils/restic/commands/restore.ts | 9 +++- e2e/0002-backup-restore.spec.ts | 53 ++++++++++++++++++- 6 files changed, 76 insertions(+), 4 deletions(-) diff --git a/app/client/api-client/types.gen.ts b/app/client/api-client/types.gen.ts index 1134d3f3..d319c8f7 100644 --- a/app/client/api-client/types.gen.ts +++ b/app/client/api-client/types.gen.ts @@ -2240,6 +2240,7 @@ export type RestoreSnapshotData = { excludeXattr?: Array; include?: Array; overwrite?: 'always' | 'if-changed' | 'if-newer' | 'never'; + selectedItemKind?: 'dir' | 'file'; targetPath?: string; }; path: { diff --git a/app/client/components/restore-form.tsx b/app/client/components/restore-form.tsx index 40573ebd..c7c48615 100644 --- a/app/client/components/restore-form.tsx +++ b/app/client/components/restore-form.tsx @@ -133,6 +133,7 @@ export function RestoreForm({ repository, snapshotId, returnPath, basePath }: Re body: { snapshotId, include: includePaths.length > 0 ? includePaths : undefined, + selectedItemKind: includePaths.length === 1 ? (selectedPathKind ?? undefined) : undefined, excludeXattr: excludeXattrArray && excludeXattrArray.length > 0 ? excludeXattrArray : undefined, targetPath, overwrite: overwriteMode, @@ -145,6 +146,7 @@ export function RestoreForm({ repository, snapshotId, returnPath, basePath }: Re restoreLocation, customTargetPath, selectedPaths, + selectedPathKind, overwriteMode, restoreSnapshot, ]); diff --git a/app/server/modules/repositories/repositories.dto.ts b/app/server/modules/repositories/repositories.dto.ts index e5958882..7d3a2d9b 100644 --- a/app/server/modules/repositories/repositories.dto.ts +++ b/app/server/modules/repositories/repositories.dto.ts @@ -375,6 +375,7 @@ export const overwriteModeSchema = type.valueOf(OVERWRITE_MODES); export const restoreSnapshotBody = type({ snapshotId: "string", include: "string[]?", + selectedItemKind: dumpPathKindSchema.optional(), exclude: "string[]?", excludeXattr: "string[]?", delete: "boolean?", diff --git a/app/server/utils/restic/commands/__tests__/restore.test.ts b/app/server/utils/restic/commands/__tests__/restore.test.ts index c73537a5..a4167649 100644 --- a/app/server/utils/restic/commands/__tests__/restore.test.ts +++ b/app/server/utils/restic/commands/__tests__/restore.test.ts @@ -100,6 +100,20 @@ describe("restore command", () => { expect(getOptionValues("--include")).toEqual([]); }); + test("restores a single selected file from its parent directory for non-root targets", async () => { + const { getRestoreArg, getOptionValues } = setup(); + const options: Parameters[3] & { selectedItemKind: "file" } = { + organizationId: "org-1", + include: ["/var/lib/zerobyte/volumes/vol123/_data/archive/backup.20260301-233001.7z"], + selectedItemKind: "file", + }; + + await restore(config, "snapshot-single-file", "/tmp/restore-target", options); + + expect(getRestoreArg()).toBe("snapshot-single-file:/var/lib/zerobyte/volumes/vol123/_data/archive"); + expect(getOptionValues("--include")).toEqual(["backup.20260301-233001.7z"]); + }); + test("does not pass an empty include when include equals restore root", async () => { const { getArgs, getRestoreArg, getOptionValues } = setup(); await restore(config, "snapshot-7202d8cc", "/Users/nicolas/Documents/restore", { diff --git a/app/server/utils/restic/commands/restore.ts b/app/server/utils/restic/commands/restore.ts index 0e283539..29f1da9e 100644 --- a/app/server/utils/restic/commands/restore.ts +++ b/app/server/utils/restic/commands/restore.ts @@ -33,6 +33,7 @@ export const restore = async ( basePath?: string; organizationId: string; include?: string[]; + selectedItemKind?: "file" | "dir"; exclude?: string[]; excludeXattr?: string[]; delete?: boolean; @@ -47,7 +48,11 @@ export const restore = async ( let restoreArg = snapshotId; const includes = options.include?.length ? options.include : [options.basePath ?? "/"]; - const commonAncestor = findCommonAncestor(includes); + const commonAncestor = + options.selectedItemKind === "file" && includes.length === 1 + ? path.posix.dirname(includes[0] ?? "/") + : findCommonAncestor(includes); + if (target !== "/") { restoreArg = `${snapshotId}:${commonAncestor}`; } @@ -64,7 +69,7 @@ export const restore = async ( args.push("--include", pattern); } } else { - const strippedIncludes = options.include.map((pattern) => path.relative(commonAncestor, pattern)); + const strippedIncludes = options.include.map((pattern) => path.posix.relative(commonAncestor, pattern)); const includesCoverRestoreRoot = strippedIncludes.some((pattern) => pattern === "" || pattern === "."); if (!includesCoverRestoreRoot) { diff --git a/e2e/0002-backup-restore.spec.ts b/e2e/0002-backup-restore.spec.ts index 5d31944a..a81e2034 100644 --- a/e2e/0002-backup-restore.spec.ts +++ b/e2e/0002-backup-restore.spec.ts @@ -30,11 +30,11 @@ function getScenarioNames(runId: string): ScenarioNames { }; } -function prepareTestFile(runId: string): string { +function prepareTestFile(runId: string, fileName = "test.json"): string { const runPath = path.join(testDataPath, runId); fs.mkdirSync(runPath, { recursive: true }); - const filePath = path.join(runPath, "test.json"); + const filePath = path.join(runPath, fileName); fs.writeFileSync(filePath, JSON.stringify({ data: "test file" })); return filePath; @@ -112,6 +112,55 @@ test("can backup & restore a file", async ({ page }, testInfo) => { expect(JSON.parse(restoredContent)).toEqual({ data: "test file" }); }); +test("can restore a single selected file to a custom location", async ({ page }, testInfo) => { + const runId = getRunId(testInfo); + const names = getScenarioNames(runId); + const fileName = `single-file-${runId}.json`; + const filePath = prepareTestFile(runId, fileName); + const restoreTargetPath = path.join(testDataPath, fileName); + const escapedFileName = fileName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + + fs.rmSync(restoreTargetPath, { force: true }); + + await gotoAndWaitForAppReady(page, "/"); + await expect(page).toHaveURL("/volumes"); + + await createBackupScenario(page, names); + + await page.getByRole("button", { name: "Backup now" }).click(); + await expect(page.getByText("Backup started successfully")).toBeVisible(); + await expect(page.getByText("✓ Success")).toBeVisible({ timeout: 30000 }); + + fs.writeFileSync(filePath, JSON.stringify({ data: "modified file" })); + + await page + .getByRole("button", { name: /\d+ B$/ }) + .first() + .click(); + await page.getByRole("link", { name: "Restore" }).click(); + await expect(page).toHaveURL(/\/restore/); + + await page.getByRole("button", { name: "Custom location" }).click(); + await page.getByRole("button", { name: "Change" }).click(); + await page.getByRole("button", { name: /^test-data$/ }).click(); + await expect(page.getByText("/test-data", { exact: true })).toBeVisible(); + + const runFolderRow = page.getByRole("button", { name: new RegExp(runId) }); + await runFolderRow.locator("svg").first().click(); + + const fileRow = page.getByRole("button", { name: new RegExp(escapedFileName) }); + await fileRow.getByRole("checkbox").click(); + await expect(page.getByText("1 item selected")).toBeVisible(); + + await page.getByRole("button", { name: "Restore 1 item" }).click(); + await expect(page.getByText("Restore completed")).toBeVisible({ timeout: 30000 }); + + const restoredContent = fs.readFileSync(restoreTargetPath, "utf8"); + expect(JSON.parse(restoredContent)).toEqual({ data: "test file" }); + + fs.rmSync(restoreTargetPath, { force: true }); +}); + test("deleting a volume cascades and removes its backup schedule", async ({ page }, testInfo) => { const runId = getRunId(testInfo); const names = getScenarioNames(runId); From b72d0af17dccdc81aef343397c23c261bcafc418 Mon Sep 17 00:00:00 2001 From: Nicolas Meienberger Date: Sun, 8 Mar 2026 15:03:39 +0100 Subject: [PATCH 07/15] docs(readme): bump version --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e90106b8..a6a4efa4 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ In order to run Zerobyte, you need to have Docker and Docker Compose installed o ```yaml services: zerobyte: - image: ghcr.io/nicotsx/zerobyte:v0.29 + image: ghcr.io/nicotsx/zerobyte:v0.30 container_name: zerobyte restart: unless-stopped cap_add: @@ -118,7 +118,7 @@ If you only need to back up locally mounted folders and don't require remote sha ```yaml services: zerobyte: - image: ghcr.io/nicotsx/zerobyte:v0.29 + image: ghcr.io/nicotsx/zerobyte:v0.30 container_name: zerobyte restart: unless-stopped ports: @@ -157,7 +157,7 @@ If you want to track a local directory on the same server where Zerobyte is runn ```diff services: zerobyte: - image: ghcr.io/nicotsx/zerobyte:v0.29 + image: ghcr.io/nicotsx/zerobyte:v0.30 container_name: zerobyte restart: unless-stopped cap_add: @@ -232,7 +232,7 @@ Zerobyte can use [rclone](https://rclone.org/) to support 40+ cloud storage prov ```diff services: zerobyte: - image: ghcr.io/nicotsx/zerobyte:v0.29 + image: ghcr.io/nicotsx/zerobyte:v0.30 container_name: zerobyte restart: unless-stopped cap_add: From 49e98d522fc0c5ed312629c5f0288ddf5674404d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 8 Mar 2026 15:11:38 +0100 Subject: [PATCH 08/15] chore(deps): bump docker/setup-buildx-action from 3 to 4 (#625) Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3 to 4. - [Release notes](https://github.com/docker/setup-buildx-action/releases) - [Commits](https://github.com/docker/setup-buildx-action/compare/v3...v4) --- updated-dependencies: - dependency-name: docker/setup-buildx-action dependency-version: '4' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/nightly.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index d57c91e9..c5ed405e 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -23,7 +23,7 @@ jobs: password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 with: driver: cloud endpoint: "meienberger/runtipi-builder" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b32a7552..e07d7561 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -55,7 +55,7 @@ jobs: password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 with: driver: cloud endpoint: "meienberger/runtipi-builder" From bdea445e4f7bdb3cfcb8a4e85daf83790dbbb4b9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 8 Mar 2026 15:11:54 +0100 Subject: [PATCH 09/15] chore(deps): bump docker/build-push-action from 6 to 7 (#627) Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6 to 7. - [Release notes](https://github.com/docker/build-push-action/releases) - [Commits](https://github.com/docker/build-push-action/compare/v6...v7) --- updated-dependencies: - dependency-name: docker/build-push-action dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/nightly.yml | 2 +- .github/workflows/release.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index c5ed405e..65f02114 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -45,7 +45,7 @@ jobs: type=raw,value=nightly - name: Push images to GitHub Container Registry - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: . target: production diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e07d7561..2262b350 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -69,7 +69,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build docker image - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: . target: production @@ -110,7 +110,7 @@ jobs: latest=${{ needs.determine-release-type.outputs.release_type == 'release' }} - name: Push images to GitHub Container Registry - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: . target: production From a4cc82f526553b27eabcd7891ebd13d4c36d8cd7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 8 Mar 2026 15:12:15 +0100 Subject: [PATCH 10/15] chore(deps): bump docker/metadata-action from 5 to 6 (#626) Bumps [docker/metadata-action](https://github.com/docker/metadata-action) from 5 to 6. - [Release notes](https://github.com/docker/metadata-action/releases) - [Commits](https://github.com/docker/metadata-action/compare/v5...v6) --- updated-dependencies: - dependency-name: docker/metadata-action dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/nightly.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 65f02114..9c7721a9 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -38,7 +38,7 @@ jobs: - name: Docker meta id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@v6 with: images: ghcr.io/${{ github.repository_owner }}/zerobyte tags: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2262b350..612e3a91 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -98,7 +98,7 @@ jobs: - name: Docker meta id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@v6 with: images: ghcr.io/${{ github.repository_owner }}/zerobyte tags: | From b35a20f43dcd25ebde6207e91acc69e68e3bdd87 Mon Sep 17 00:00:00 2001 From: Nico <47644445+nicotsx@users.noreply.github.com> Date: Mon, 9 Mar 2026 18:16:50 +0100 Subject: [PATCH 11/15] feat: light theme (#638) --- .oxlintrc.json | 1 + app/app.css | 70 ++++++++++++------- app/client/components/app-sidebar.tsx | 4 +- app/client/components/cron-input.tsx | 2 +- app/client/components/grid-background.tsx | 2 +- app/client/components/layout.tsx | 6 +- app/client/components/onoff.tsx | 2 +- .../components/organization-switcher.tsx | 2 +- app/client/components/status-dot.tsx | 4 +- app/client/components/theme-provider.tsx | 35 ++++++++++ app/client/components/theme-toggle.tsx | 26 +++++++ app/client/components/ui/button.tsx | 7 +- app/client/components/ui/card.tsx | 18 ++--- app/client/components/ui/code-block.tsx | 6 +- app/client/components/ui/sonner.tsx | 2 +- app/client/components/ui/tabs.tsx | 14 ++-- .../backups/components/backup-card.tsx | 18 ++--- .../backups/components/snapshot-timeline.tsx | 5 +- .../modules/backups/routes/backup-details.tsx | 4 +- .../routes/notification-details.tsx | 2 +- .../repositories/components/doctor-report.tsx | 2 +- .../repositories/routes/repositories.tsx | 2 +- app/client/modules/repositories/tabs/info.tsx | 6 +- .../components/two-factor-section.tsx | 2 +- .../settings/components/user-management.tsx | 2 +- .../volumes/components/create-volume-form.tsx | 4 +- .../volumes/components/healthchecks-card.tsx | 2 +- app/client/modules/volumes/routes/volumes.tsx | 2 +- app/routes/__root.tsx | 25 +++++-- app/server/core/__tests__/scheduler.test.ts | 1 + 30 files changed, 189 insertions(+), 89 deletions(-) create mode 100644 app/client/components/theme-provider.tsx create mode 100644 app/client/components/theme-toggle.tsx diff --git a/.oxlintrc.json b/.oxlintrc.json index ae1c8af2..f2e5a583 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -114,6 +114,7 @@ "unicorn/prefer-set-size": "warn", "unicorn/prefer-string-starts-ends-with": "warn", "import/no-cycle": "error", + "import/no-unused-modules": ["error", { "unusedExports": true }], "eslint/no-console": ["warn", { "allow": ["warn", "error", "info"] }] }, "settings": { diff --git a/app/app.css b/app/app.css index 15a1e65c..15f07d91 100644 --- a/app/app.css +++ b/app/app.css @@ -37,7 +37,6 @@ body { } body { - @apply bg-[#131313]; min-height: 100dvh; } @@ -67,6 +66,8 @@ body { --color-accent: var(--accent); --color-accent-foreground: var(--accent-foreground); --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-success: var(--success); --color-border: var(--border); --color-input: var(--input); --color-ring: var(--ring); @@ -88,38 +89,40 @@ body { :root { --radius: 0.625rem; - --background: oklch(1 0 0); - --foreground: oklch(0.145 0 0); - --card: oklch(1 0 0); - --card-foreground: oklch(0.145 0 0); - --card-header: oklch(0.922 0 0); - --popover: oklch(1 0 0); - --popover-foreground: oklch(0.145 0 0); - --primary: oklch(0.205 0 0); - --primary-foreground: oklch(0.985 0 0); - --secondary: oklch(0.97 0 0); - --secondary-foreground: oklch(0.205 0 0); - --muted: oklch(0.97 0 0); - --muted-foreground: oklch(0.556 0 0); - --accent: oklch(0.97 0 0); - --accent-foreground: oklch(0.205 0 0); + --background: oklch(0.975 0.005 80); + --foreground: oklch(0.25 0.015 55); + --card: oklch(0.988 0.004 80); + --card-foreground: oklch(0.25 0.015 55); + --card-header: oklch(0.945 0.008 80); + --popover: oklch(0.988 0.004 80); + --popover-foreground: oklch(0.25 0.015 55); + --primary: oklch(0.25 0.015 55); + --primary-foreground: oklch(0.975 0.005 80); + --secondary: oklch(0.935 0.008 70); + --secondary-foreground: oklch(0.25 0.015 55); + --muted: oklch(0.935 0.008 70); + --muted-foreground: oklch(0.58 0.025 60); + --accent: oklch(0.935 0.008 70); + --accent-foreground: oklch(0.25 0.015 55); --destructive: oklch(0.577 0.245 27.325); - --border: oklch(0.922 0 0); - --input: oklch(0.922 0 0); - --ring: oklch(0.708 0 0); + --destructive-foreground: #fff; + --success: oklch(0.57 0.16 148); + --border: oklch(0.9 0.01 70); + --input: oklch(0.9 0.01 70); + --ring: oklch(0.7 0.015 60); --chart-1: oklch(0.646 0.222 41.116); --chart-2: oklch(0.6 0.118 184.704); --chart-3: oklch(0.398 0.07 227.392); --chart-4: oklch(0.828 0.189 84.429); --chart-5: oklch(0.769 0.188 70.08); - --sidebar: oklch(0.985 0 0); - --sidebar-foreground: oklch(0.145 0 0); - --sidebar-primary: oklch(0.205 0 0); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.97 0 0); - --sidebar-accent-foreground: oklch(0.205 0 0); - --sidebar-border: oklch(0.922 0 0); - --sidebar-ring: oklch(0.708 0 0); + --sidebar: oklch(0.96 0.007 75); + --sidebar-foreground: oklch(0.25 0.015 55); + --sidebar-primary: oklch(0.25 0.015 55); + --sidebar-primary-foreground: oklch(0.975 0.005 80); + --sidebar-accent: oklch(0.935 0.008 70); + --sidebar-accent-foreground: oklch(0.25 0.015 55); + --sidebar-border: oklch(0.9 0.01 70); + --sidebar-ring: oklch(0.7 0.015 60); --strong-accent: #ff543a; } @@ -144,6 +147,8 @@ body { --accent: oklch(0.269 0 0); --accent-foreground: oklch(0.985 0 0); --destructive: #ff543a; + --destructive-foreground: #fff; + --success: oklch(0.696 0.17 162.48); --border: oklch(1 0 0 / 10%); --input: oklch(1 0 0 / 15%); --ring: oklch(0.556 0 0); @@ -190,6 +195,17 @@ body { } } +.snapshot-scrollable::-webkit-scrollbar { + height: 6px; +} +.snapshot-scrollable::-webkit-scrollbar-track { + background: transparent; +} +.snapshot-scrollable::-webkit-scrollbar-thumb { + background: var(--border); + border-radius: 999px; +} + /* Hide built-in password reveal/clear controls (notably in Edge on Windows) */ [data-secret-input] input[type="password"]::-ms-reveal, [data-secret-input] input[type="password"]::-ms-clear { diff --git a/app/client/components/app-sidebar.tsx b/app/client/components/app-sidebar.tsx index 5bb686ec..e59b397c 100644 --- a/app/client/components/app-sidebar.tsx +++ b/app/client/components/app-sidebar.tsx @@ -115,7 +115,7 @@ export function AppSidebar({ isInstanceAdmin }: Props) { /> @@ -171,7 +171,7 @@ export function AppSidebar({ isInstanceAdmin }: Props) { /> diff --git a/app/client/components/cron-input.tsx b/app/client/components/cron-input.tsx index c7e85e06..35e387ed 100644 --- a/app/client/components/cron-input.tsx +++ b/app/client/components/cron-input.tsx @@ -59,7 +59,7 @@ export function CronInput({ value, onChange, error }: CronInputProps) { {value && (
{isValid ? ( - + ) : ( )} diff --git a/app/client/components/grid-background.tsx b/app/client/components/grid-background.tsx index ec417f3a..0eadcb29 100644 --- a/app/client/components/grid-background.tsx +++ b/app/client/components/grid-background.tsx @@ -14,7 +14,7 @@ export function GridBackground({ children, className, containerClassName }: Grid className={cn( "pointer-events-none absolute inset-0 w-full h-full", "bg-[size:40px_40px]", - "bg-[linear-gradient(to_right,#e4e4e7_1px,transparent_1px),linear-gradient(to_bottom,#e4e4e7_1px,transparent_1px)]", + "bg-[linear-gradient(to_right,#EAE5DF_1px,transparent_1px),linear-gradient(to_bottom,#EAE5DF_1px,transparent_1px)]", "dark:bg-[linear-gradient(to_right,#262626_1px,transparent_1px),linear-gradient(to_bottom,#262626_1px,transparent_1px)]", "[mask-image:radial-gradient(ellipse_at_top,black_70%,transparent_100%)]", )} diff --git a/app/client/components/layout.tsx b/app/client/components/layout.tsx index 82600947..8b16ff22 100644 --- a/app/client/components/layout.tsx +++ b/app/client/components/layout.tsx @@ -10,6 +10,7 @@ import { DevPanelListener } from "./dev-panel-listener"; import { Outlet, useNavigate } from "@tanstack/react-router"; import { AppBreadcrumb } from "./app-breadcrumb"; import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip"; +import { ThemeToggle } from "./theme-toggle"; type Props = { loaderData: AppContext; @@ -46,12 +47,13 @@ export function Layout({ loaderData }: Props) { {loaderData.user.name} + + + Toggle theme + + ); +} diff --git a/app/client/components/ui/button.tsx b/app/client/components/ui/button.tsx index 9f2411b3..6908fba4 100644 --- a/app/client/components/ui/button.tsx +++ b/app/client/components/ui/button.tsx @@ -11,13 +11,12 @@ const buttonVariants = cva( { variants: { variant: { - default: - "bg-transparent text-white hover:bg-[#3A3A3A]/80 border hover:border-white/40 dark:text-white dark:hover:bg-[#3A3A3A]/80", + default: "bg-transparent text-foreground hover:bg-muted border hover:border-border", primary: "bg-strong-accent text-white hover:bg-strong-accent/90 focus-visible:ring-strong-accent/50", destructive: - "border border-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/50 text-destructive hover:text-white", + "border border-destructive text-destructive hover:bg-destructive hover:text-destructive-foreground focus-visible:ring-destructive/50", outline: "border border-border bg-background hover:bg-accent hover:text-accent-foreground", - secondary: "bg-transparent text-white hover:bg-[#3A3A3A]/80 border dark:text-white dark:hover:bg-[#3A3A3A]/80", + secondary: "bg-transparent text-foreground hover:bg-muted border", ghost: "hover:bg-accent hover:text-accent-foreground", link: "text-primary underline-offset-4 hover:underline", }, diff --git a/app/client/components/ui/card.tsx b/app/client/components/ui/card.tsx index 00aa1e72..ed6393a7 100644 --- a/app/client/components/ui/card.tsx +++ b/app/client/components/ui/card.tsx @@ -7,7 +7,7 @@ function Card({ className, children, interactive, ...props }: React.ComponentPro
- - - - - - - - + + + + + + + + {children}
diff --git a/app/client/components/ui/code-block.tsx b/app/client/components/ui/code-block.tsx index c8529a0d..9be46dab 100644 --- a/app/client/components/ui/code-block.tsx +++ b/app/client/components/ui/code-block.tsx @@ -8,8 +8,8 @@ interface CodeBlockProps { export const CodeBlock: React.FC = ({ code, filename }) => { return ( -
-
+
+
@@ -18,7 +18,7 @@ export const CodeBlock: React.FC = ({ code, filename }) => {
-				{code}
+				{code}
 			
); diff --git a/app/client/components/ui/sonner.tsx b/app/client/components/ui/sonner.tsx index 30810352..abcd1c27 100644 --- a/app/client/components/ui/sonner.tsx +++ b/app/client/components/ui/sonner.tsx @@ -1,4 +1,4 @@ -import { useTheme } from "next-themes"; +import { useTheme } from "~/client/components/theme-provider"; import { Toaster as Sonner, type ToasterProps } from "sonner"; const Toaster = ({ ...props }: ToasterProps) => { diff --git a/app/client/components/ui/tabs.tsx b/app/client/components/ui/tabs.tsx index cab7b27a..e965b0e8 100644 --- a/app/client/components/ui/tabs.tsx +++ b/app/client/components/ui/tabs.tsx @@ -37,11 +37,11 @@ function TabsTrigger({ className, onClick, ...props }: React.ComponentProps {props.children} {/* Left bracket - bottom tick */} - + {/* Right bracket - top tick */} - + {/* Right bracket - vertical line */} - + {/* Right bracket - bottom tick */} - + ); } diff --git a/app/client/modules/backups/components/backup-card.tsx b/app/client/modules/backups/components/backup-card.tsx index 2bf7b337..16f529c8 100644 --- a/app/client/modules/backups/components/backup-card.tsx +++ b/app/client/modules/backups/components/backup-card.tsx @@ -32,21 +32,21 @@ export const BackupCard = ({ schedule }: { schedule: BackupSchedule }) => {
- Schedule -
- + Schedule +
+ {schedule.cronExpression}
- Last backup -
- {formatTimeAgo(schedule.lastBackupAt)} + Last backup +
+ {formatTimeAgo(schedule.lastBackupAt)}
- Next backup -
- + Next backup +
+ {formatShortDateTime(schedule.nextBackupAt)}
diff --git a/app/client/modules/backups/components/snapshot-timeline.tsx b/app/client/modules/backups/components/snapshot-timeline.tsx index 11581e96..cb4b5286 100644 --- a/app/client/modules/backups/components/snapshot-timeline.tsx +++ b/app/client/modules/backups/components/snapshot-timeline.tsx @@ -1,3 +1,4 @@ +import { useRef } from "react"; import type { ListSnapshotsResponse } from "~/client/api-client"; import { ByteSize } from "~/client/components/bytes-size"; import { Card, CardContent } from "~/client/components/ui/card"; @@ -15,6 +16,7 @@ interface Props { export const SnapshotTimeline = (props: Props) => { const { snapshots, snapshotId, loading, onSnapshotSelect, error } = props; + const selectedRef = useRef(null); if (error) { return ( @@ -51,13 +53,14 @@ export const SnapshotTimeline = (props: Props) => {
-
+
{snapshots.map((snapshot) => { const date = new Date(snapshot.time); const isSelected = snapshotId === snapshot.short_id; return (