From 9582622ed2c89de79a37439ef0ca749b9faa48c7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jakub=20Tr=C3=A1vn=C3=ADk?=
Date: Mon, 5 Jan 2026 10:17:22 +0100
Subject: [PATCH 01/11] feat: add .editorconfig and .gitattributes and edited
biome.json for consistent formatting and line endings
---
.editorconfig | 14 ++++++++++++++
.gitattributes | 28 ++++++++++++++++++++++++++++
biome.json | 3 ++-
3 files changed, 44 insertions(+), 1 deletion(-)
create mode 100644 .editorconfig
create mode 100644 .gitattributes
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 00000000..65f8370a
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,14 @@
+root = true
+
+[*]
+end_of_line = lf
+charset = utf-8
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.{ts,tsx,js,json}]
+indent_style = tab
+indent_size = 4
+
+[*.md]
+trim_trailing_whitespace = false
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 00000000..becb667a
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,28 @@
+# Set default behavior to automatically normalize line endings
+* text=auto eol=lf
+
+# Explicitly declare text files
+*.ts text eol=lf
+*.tsx text eol=lf
+*.js text eol=lf
+*.json text eol=lf
+*.md text eol=lf
+*.css text eol=lf
+*.html text eol=lf
+*.yml text eol=lf
+*.yaml text eol=lf
+*.sql text eol=lf
+*.sh text eol=lf
+*.toml text eol=lf
+
+# Binary files
+*.png binary
+*.jpg binary
+*.jpeg binary
+*.gif binary
+*.ico binary
+*.webp binary
+*.woff binary
+*.woff2 binary
+*.ttf binary
+*.eot binary
diff --git a/biome.json b/biome.json
index 928a49a7..602097c3 100644
--- a/biome.json
+++ b/biome.json
@@ -13,7 +13,8 @@
"formatter": {
"enabled": true,
"indentStyle": "tab",
- "lineWidth": 120
+ "lineWidth": 120,
+ "lineEnding": "lf"
},
"linter": {
"enabled": true,
From f04d976cd8f2e297ae06a68b1c323420eb03176a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jakub=20Tr=C3=A1vn=C3=ADk?=
Date: Mon, 5 Jan 2026 10:26:53 +0100
Subject: [PATCH 02/11] changed to tab_width
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
.editorconfig | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.editorconfig b/.editorconfig
index 65f8370a..54c5265e 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -8,7 +8,7 @@ trim_trailing_whitespace = true
[*.{ts,tsx,js,json}]
indent_style = tab
-indent_size = 4
+tab_width = 4
[*.md]
trim_trailing_whitespace = false
From 4671ebfc7951cca1a0234cd72ec8f30ae1f7e8fe Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jakub=20Tr=C3=A1vn=C3=ADk?=
Date: Mon, 5 Jan 2026 10:27:39 +0100
Subject: [PATCH 03/11] added more explicit text file types
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
.gitattributes | 3 +++
1 file changed, 3 insertions(+)
diff --git a/.gitattributes b/.gitattributes
index becb667a..5e4c8497 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -14,6 +14,9 @@
*.sql text eol=lf
*.sh text eol=lf
*.toml text eol=lf
+Dockerfile* text eol=lf
+.dockerignore text eol=lf
+docker-compose*.yml text eol=lf
# Binary files
*.png binary
From 7f71767ce7cd3cfbba13c923a54c371a130b646d Mon Sep 17 00:00:00 2001
From: Nico <47644445+nicotsx@users.noreply.github.com>
Date: Mon, 5 Jan 2026 20:46:36 +0100
Subject: [PATCH 04/11] refactor(notifications): add more context in title
(#296)
* refactor(notifications): add more context in title
* chore: pr feedback
---
.../notifications/notifications.service.ts | 18 ++++++------------
1 file changed, 6 insertions(+), 12 deletions(-)
diff --git a/app/server/modules/notifications/notifications.service.ts b/app/server/modules/notifications/notifications.service.ts
index 4992ba3e..e927e9ac 100644
--- a/app/server/modules/notifications/notifications.service.ts
+++ b/app/server/modules/notifications/notifications.service.ts
@@ -369,18 +369,16 @@ function buildNotificationMessage(
snapshotId?: string;
},
) {
- const date = new Date().toLocaleDateString();
- const time = new Date().toLocaleTimeString();
+ const backupName = context.scheduleName ?? "backup";
switch (event) {
case "start":
return {
- title: "🔵 Backup Started",
+ title: `Zerobyte ${backupName} started`,
body: [
`Volume: ${context.volumeName}`,
`Repository: ${context.repositoryName}`,
context.scheduleName ? `Schedule: ${context.scheduleName}` : null,
- `Time: ${date} - ${time}`,
]
.filter(Boolean)
.join("\n"),
@@ -388,7 +386,7 @@ function buildNotificationMessage(
case "success":
return {
- title: "✅ Backup Completed successfully",
+ title: `Zerobyte ${backupName} completed successfully`,
body: [
`Volume: ${context.volumeName}`,
`Repository: ${context.repositoryName}`,
@@ -397,7 +395,6 @@ function buildNotificationMessage(
context.filesProcessed !== undefined ? `Files: ${context.filesProcessed}` : null,
context.bytesProcessed ? `Size: ${context.bytesProcessed}` : null,
context.snapshotId ? `Snapshot: ${context.snapshotId}` : null,
- `Time: ${date} - ${time}`,
]
.filter(Boolean)
.join("\n"),
@@ -405,7 +402,7 @@ function buildNotificationMessage(
case "warning":
return {
- title: "! Backup completed with warnings",
+ title: `Zerobyte ${backupName} completed with warnings`,
body: [
`Volume: ${context.volumeName}`,
`Repository: ${context.repositoryName}`,
@@ -415,7 +412,6 @@ function buildNotificationMessage(
context.bytesProcessed ? `Size: ${context.bytesProcessed}` : null,
context.snapshotId ? `Snapshot: ${context.snapshotId}` : null,
context.error ? `Warning: ${context.error}` : null,
- `Time: ${date} - ${time}`,
]
.filter(Boolean)
.join("\n"),
@@ -423,13 +419,12 @@ function buildNotificationMessage(
case "failure":
return {
- title: "❌ Backup failed",
+ title: `Zerobyte ${backupName} failed`,
body: [
`Volume: ${context.volumeName}`,
`Repository: ${context.repositoryName}`,
context.scheduleName ? `Schedule: ${context.scheduleName}` : null,
context.error ? `Error: ${context.error}` : null,
- `Time: ${date} - ${time}`,
]
.filter(Boolean)
.join("\n"),
@@ -437,12 +432,11 @@ function buildNotificationMessage(
default:
return {
- title: "Backup Notification",
+ title: `Zerobyte ${backupName} notification`,
body: [
`Volume: ${context.volumeName}`,
`Repository: ${context.repositoryName}`,
context.scheduleName ? `Schedule: ${context.scheduleName}` : null,
- `Time: ${date} - ${time}`,
]
.filter(Boolean)
.join("\n"),
From 1391d6fe5e063d4d3c78eebeda9d06259e0697b1 Mon Sep 17 00:00:00 2001
From: Nico <47644445+nicotsx@users.noreply.github.com>
Date: Mon, 5 Jan 2026 21:04:37 +0100
Subject: [PATCH 05/11] refactor: move from biome to oxlint/oxformat (#311)
* chore: install and configure oxlint
* chore: fix liniting issues
* chore: install and configure oxfmt
* ci: add oxlint action instead of biome
* chore: pr feedbacks
* Revert "chore: pr feedbacks"
This reverts commit 525dcd8d9f54e897b7c0208d02b1c561009dcdc1.
---
.github/workflows/checks.yml | 7 +-
.oxfmtrc.json | 5 +
.oxlintrc.json | 150 ++++++++++++++++++
app/client/components/directory-browser.tsx | 2 +-
app/client/components/layout.tsx | 2 +-
app/client/components/restore-form.tsx | 4 +-
app/client/components/snapshots-table.tsx | 6 +-
app/client/components/volume-file-browser.tsx | 2 +-
app/client/hooks/use-server-events.ts | 10 +-
.../auth/routes/download-recovery-key.tsx | 2 +-
app/client/modules/auth/routes/login.tsx | 4 +-
app/client/modules/auth/routes/onboarding.tsx | 2 +-
.../components/create-schedule-form.tsx | 2 +-
.../components/snapshot-file-browser.tsx | 2 +-
.../modules/backups/routes/backup-details.tsx | 2 +-
.../modules/backups/routes/create-backup.tsx | 2 +-
.../routes/create-notification.tsx | 2 +-
.../routes/notification-details.tsx | 4 +-
.../repositories/routes/create-repository.tsx | 2 +-
.../routes/repository-details.tsx | 4 +-
.../modules/settings/routes/settings.tsx | 2 +-
.../modules/volumes/routes/create-volume.tsx | 2 +-
.../modules/volumes/routes/volume-details.tsx | 2 +-
app/client/modules/volumes/tabs/info.tsx | 2 +-
app/root.tsx | 4 +-
app/server/core/scheduler.ts | 4 +-
app/server/index.ts | 4 +-
app/server/jobs/cleanup-sessions.ts | 2 +-
.../backups/__tests__/backups.service.test.ts | 2 +-
app/server/modules/backups/backups.service.ts | 2 +-
.../modules/events/events.controller.ts | 32 ++--
app/server/utils/logger.ts | 4 +-
app/server/utils/rclone.ts | 7 +-
app/server/utils/restic.ts | 18 +--
app/test/setup.ts | 2 +-
biome.json | 43 -----
bun.lock | 74 ++++++---
components.json | 10 +-
package.json | 7 +-
tsconfig.json | 1 -
40 files changed, 294 insertions(+), 146 deletions(-)
create mode 100644 .oxfmtrc.json
create mode 100644 .oxlintrc.json
delete mode 100644 biome.json
diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml
index ba422b66..3c88d2ca 100644
--- a/.github/workflows/checks.yml
+++ b/.github/workflows/checks.yml
@@ -25,9 +25,10 @@ jobs:
- name: Install dependencies
uses: "./.github/actions/install-dependencies"
- - name: Run lint
- shell: bash
- run: bun run lint:ci
+ - uses: oxc-project/oxlint-action@latest
+ with:
+ config: .oxlintrc.json
+ deny-warnings: true
- name: Run type checks
shell: bash
diff --git a/.oxfmtrc.json b/.oxfmtrc.json
new file mode 100644
index 00000000..98859c34
--- /dev/null
+++ b/.oxfmtrc.json
@@ -0,0 +1,5 @@
+{
+ "$schema": "./node_modules/oxfmt/configuration_schema.json",
+ "printWidth": 120,
+ "useTabs": true
+}
diff --git a/.oxlintrc.json b/.oxlintrc.json
new file mode 100644
index 00000000..634c4b7c
--- /dev/null
+++ b/.oxlintrc.json
@@ -0,0 +1,150 @@
+{
+ "$schema": "./node_modules/oxlint/configuration_schema.json",
+ "plugins": ["unicorn", "typescript", "oxc"],
+ "categories": {},
+ "rules": {
+ "constructor-super": "warn",
+ "for-direction": "warn",
+ "no-async-promise-executor": "warn",
+ "no-caller": "warn",
+ "no-class-assign": "warn",
+ "no-compare-neg-zero": "warn",
+ "no-cond-assign": "warn",
+ "no-const-assign": "warn",
+ "no-constant-binary-expression": "warn",
+ "no-constant-condition": "warn",
+ "no-control-regex": "warn",
+ "no-debugger": "warn",
+ "no-delete-var": "warn",
+ "no-dupe-class-members": "warn",
+ "no-dupe-else-if": "warn",
+ "no-dupe-keys": "warn",
+ "no-duplicate-case": "warn",
+ "no-empty-character-class": "warn",
+ "no-empty-pattern": "warn",
+ "no-empty-static-block": "warn",
+ "no-eval": "warn",
+ "no-ex-assign": "warn",
+ "no-extra-boolean-cast": "warn",
+ "no-func-assign": "warn",
+ "no-global-assign": "warn",
+ "no-import-assign": "warn",
+ "no-invalid-regexp": "warn",
+ "no-irregular-whitespace": "warn",
+ "no-loss-of-precision": "warn",
+ "no-new-native-nonconstructor": "warn",
+ "no-nonoctal-decimal-escape": "warn",
+ "no-obj-calls": "warn",
+ "no-self-assign": "warn",
+ "no-setter-return": "warn",
+ "no-shadow-restricted-names": "warn",
+ "no-sparse-arrays": "warn",
+ "no-this-before-super": "warn",
+ "no-unassigned-vars": "warn",
+ "no-unsafe-finally": "warn",
+ "no-unsafe-negation": "warn",
+ "no-unsafe-optional-chaining": "warn",
+ "no-unused-expressions": "warn",
+ "no-unused-labels": "warn",
+ "no-unused-private-class-members": "warn",
+ "no-unused-vars": [
+ "warn",
+ {
+ "caughtErrorsIgnorePattern": "^_",
+ "argsIgnorePattern": "^_"
+ }
+ ],
+ "no-useless-backreference": "warn",
+ "no-useless-catch": "warn",
+ "no-useless-escape": "warn",
+ "no-useless-rename": "warn",
+ "no-with": "warn",
+ "require-yield": "warn",
+ "use-isnan": "warn",
+ "valid-typeof": "warn",
+ "oxc/bad-array-method-on-arguments": "warn",
+ "oxc/bad-char-at-comparison": "warn",
+ "oxc/bad-comparison-sequence": "warn",
+ "oxc/bad-min-max-func": "warn",
+ "oxc/bad-object-literal-comparison": "warn",
+ "oxc/bad-replace-all-arg": "warn",
+ "oxc/const-comparisons": "warn",
+ "oxc/double-comparisons": "warn",
+ "oxc/erasing-op": "warn",
+ "oxc/missing-throw": "warn",
+ "oxc/number-arg-out-of-range": "warn",
+ "oxc/only-used-in-recursion": "warn",
+ "oxc/uninvoked-array-callback": "warn",
+ "typescript/await-thenable": "warn",
+ "typescript/no-array-delete": "warn",
+ "typescript/no-base-to-string": "warn",
+ "typescript/no-duplicate-enum-values": "warn",
+ "typescript/no-duplicate-type-constituents": "warn",
+ "typescript/no-extra-non-null-assertion": "warn",
+ "typescript/no-floating-promises": "warn",
+ "typescript/no-for-in-array": "warn",
+ "typescript/no-implied-eval": "warn",
+ "typescript/no-meaningless-void-operator": "warn",
+ "typescript/no-misused-new": "warn",
+ "typescript/no-misused-spread": "warn",
+ "typescript/no-non-null-asserted-optional-chain": "warn",
+ "typescript/no-redundant-type-constituents": "warn",
+ "typescript/no-this-alias": "warn",
+ "typescript/no-unnecessary-parameter-property-assignment": "warn",
+ "typescript/no-unsafe-declaration-merging": "warn",
+ "typescript/no-unsafe-unary-minus": "warn",
+ "typescript/no-useless-empty-export": "warn",
+ "typescript/no-wrapper-object-types": "warn",
+ "typescript/prefer-as-const": "warn",
+ "typescript/require-array-sort-compare": "warn",
+ "typescript/restrict-template-expressions": "warn",
+ "typescript/triple-slash-reference": "warn",
+ "typescript/unbound-method": "warn",
+ "unicorn/no-await-in-promise-methods": "warn",
+ "unicorn/no-empty-file": "warn",
+ "unicorn/no-invalid-fetch-options": "warn",
+ "unicorn/no-invalid-remove-event-listener": "warn",
+ "unicorn/no-new-array": "warn",
+ "unicorn/no-single-promise-in-promise-methods": "warn",
+ "unicorn/no-thenable": "warn",
+ "unicorn/no-unnecessary-await": "warn",
+ "unicorn/no-useless-fallback-in-spread": "warn",
+ "unicorn/no-useless-length-check": "warn",
+ "unicorn/no-useless-spread": "warn",
+ "unicorn/prefer-set-size": "warn",
+ "unicorn/prefer-string-starts-ends-with": "warn"
+ },
+ "settings": {
+ "jsx-a11y": {
+ "polymorphicPropName": null,
+ "components": {},
+ "attributes": {}
+ },
+ "next": {
+ "rootDir": []
+ },
+ "react": {
+ "formComponents": [],
+ "linkComponents": [],
+ "version": null
+ },
+ "jsdoc": {
+ "ignorePrivate": false,
+ "ignoreInternal": false,
+ "ignoreReplacesDocs": true,
+ "overrideReplacesDocs": true,
+ "augmentsExtendsReplacesDocs": false,
+ "implementsReplacesDocs": false,
+ "exemptDestructuredRootsFromChecks": false,
+ "tagNamePreference": {}
+ },
+ "vitest": {
+ "typecheck": false
+ }
+ },
+ "env": {
+ "builtin": true
+ },
+ "globals": {},
+ "ignorePatterns": ["**/api-client/**"]
+}
diff --git a/app/client/components/directory-browser.tsx b/app/client/components/directory-browser.tsx
index 002b8a47..bfea00b3 100644
--- a/app/client/components/directory-browser.tsx
+++ b/app/client/components/directory-browser.tsx
@@ -23,7 +23,7 @@ export const DirectoryBrowser = ({ onSelectPath, selectedPath }: Props) => {
return await queryClient.ensureQueryData(browseFilesystemOptions({ query: { path } }));
},
prefetchFolder: (path) => {
- queryClient.prefetchQuery(browseFilesystemOptions({ query: { path } }));
+ void queryClient.prefetchQuery(browseFilesystemOptions({ query: { path } }));
},
});
diff --git a/app/client/components/layout.tsx b/app/client/components/layout.tsx
index 6030391d..3868bd9e 100644
--- a/app/client/components/layout.tsx
+++ b/app/client/components/layout.tsx
@@ -30,7 +30,7 @@ export default function Layout({ loaderData }: Route.ComponentProps) {
const logout = useMutation({
...logoutMutation(),
onSuccess: async () => {
- navigate("/login", { replace: true });
+ void navigate("/login", { replace: true });
},
onError: (error) => {
console.error(error);
diff --git a/app/client/components/restore-form.tsx b/app/client/components/restore-form.tsx
index 55a6d0ce..37d6d55e 100644
--- a/app/client/components/restore-form.tsx
+++ b/app/client/components/restore-form.tsx
@@ -83,7 +83,7 @@ export function RestoreForm({ snapshot, repository, snapshotId, returnPath }: Re
);
},
prefetchFolder: (path) => {
- queryClient.prefetchQuery(
+ void queryClient.prefetchQuery(
listSnapshotFilesOptions({
path: { id: repository.id, snapshotId },
query: { path },
@@ -102,7 +102,7 @@ export function RestoreForm({ snapshot, repository, snapshotId, returnPath }: Re
toast.success("Restore completed", {
description: `Successfully restored ${data.filesRestored} file(s). ${data.filesSkipped} file(s) skipped.`,
});
- navigate(returnPath);
+ void navigate(returnPath);
},
onError: (error) => {
toast.error("Restore failed", { description: error.message || "Failed to restore snapshot" });
diff --git a/app/client/components/snapshots-table.tsx b/app/client/components/snapshots-table.tsx
index 0f8574bb..2e3b7f79 100644
--- a/app/client/components/snapshots-table.tsx
+++ b/app/client/components/snapshots-table.tsx
@@ -50,7 +50,7 @@ export const SnapshotsTable = ({ snapshots, repositoryId, backups }: Props) => {
const deleteSnapshots = useMutation({
...deleteSnapshotsMutation(),
onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ["listSnapshots"] });
+ void queryClient.invalidateQueries({ queryKey: ["listSnapshots"] });
setShowBulkDeleteConfirm(false);
setSelectedIds(new Set());
},
@@ -62,7 +62,7 @@ export const SnapshotsTable = ({ snapshots, repositoryId, backups }: Props) => {
setShowReTagDialog(false);
},
onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ["listSnapshots"] });
+ void queryClient.invalidateQueries({ queryKey: ["listSnapshots"] });
setShowReTagDialog(false);
setSelectedIds(new Set());
setTargetScheduleId("");
@@ -70,7 +70,7 @@ export const SnapshotsTable = ({ snapshots, repositoryId, backups }: Props) => {
});
const handleRowClick = (snapshotId: string) => {
- navigate(`/repositories/${repositoryId}/${snapshotId}`);
+ void navigate(`/repositories/${repositoryId}/${snapshotId}`);
};
const toggleSelectAll = () => {
diff --git a/app/client/components/volume-file-browser.tsx b/app/client/components/volume-file-browser.tsx
index 6e9cd53e..78327270 100644
--- a/app/client/components/volume-file-browser.tsx
+++ b/app/client/components/volume-file-browser.tsx
@@ -46,7 +46,7 @@ export const VolumeFileBrowser = ({
);
},
prefetchFolder: (path) => {
- queryClient.prefetchQuery(
+ void queryClient.prefetchQuery(
listFilesOptions({
path: { name: volumeName },
query: { path },
diff --git a/app/client/hooks/use-server-events.ts b/app/client/hooks/use-server-events.ts
index d7c03777..211d33dc 100644
--- a/app/client/hooks/use-server-events.ts
+++ b/app/client/hooks/use-server-events.ts
@@ -87,8 +87,8 @@ export function useServerEvents() {
const data = JSON.parse(e.data) as BackupEvent;
console.log("[SSE] Backup completed:", data);
- queryClient.invalidateQueries();
- queryClient.refetchQueries();
+ void queryClient.invalidateQueries();
+ void queryClient.refetchQueries();
handlersRef.current.get("backup:completed")?.forEach((handler) => {
handler(data);
@@ -117,7 +117,7 @@ export function useServerEvents() {
const data = JSON.parse(e.data) as VolumeEvent;
console.log("[SSE] Volume updated:", data);
- queryClient.invalidateQueries();
+ void queryClient.invalidateQueries();
handlersRef.current.get("volume:updated")?.forEach((handler) => {
handler(data);
@@ -128,7 +128,7 @@ export function useServerEvents() {
const data = JSON.parse(e.data) as VolumeEvent;
console.log("[SSE] Volume status updated:", data);
- queryClient.invalidateQueries();
+ void queryClient.invalidateQueries();
handlersRef.current.get("volume:updated")?.forEach((handler) => {
handler(data);
@@ -149,7 +149,7 @@ export function useServerEvents() {
console.log("[SSE] Mirror copy completed:", data);
// Invalidate queries to refresh mirror status in the UI
- queryClient.invalidateQueries();
+ void queryClient.invalidateQueries();
handlersRef.current.get("mirror:completed")?.forEach((handler) => {
handler(data);
diff --git a/app/client/modules/auth/routes/download-recovery-key.tsx b/app/client/modules/auth/routes/download-recovery-key.tsx
index 5235702a..af29dd05 100644
--- a/app/client/modules/auth/routes/download-recovery-key.tsx
+++ b/app/client/modules/auth/routes/download-recovery-key.tsx
@@ -42,7 +42,7 @@ export default function DownloadRecoveryKeyPage() {
window.URL.revokeObjectURL(url);
toast.success("Recovery key downloaded successfully!");
- navigate("/volumes", { replace: true });
+ void navigate("/volumes", { replace: true });
},
onError: (error) => {
toast.error("Failed to download recovery key", { description: error.message });
diff --git a/app/client/modules/auth/routes/login.tsx b/app/client/modules/auth/routes/login.tsx
index b62e0cca..984d8130 100644
--- a/app/client/modules/auth/routes/login.tsx
+++ b/app/client/modules/auth/routes/login.tsx
@@ -49,9 +49,9 @@ export default function LoginPage() {
...loginMutation(),
onSuccess: async (data) => {
if (data.user && !data.user.hasDownloadedResticPassword) {
- navigate("/download-recovery-key");
+ void navigate("/download-recovery-key");
} else {
- navigate("/volumes");
+ void navigate("/volumes");
}
},
onError: (error) => {
diff --git a/app/client/modules/auth/routes/onboarding.tsx b/app/client/modules/auth/routes/onboarding.tsx
index cb72ed60..f95356f9 100644
--- a/app/client/modules/auth/routes/onboarding.tsx
+++ b/app/client/modules/auth/routes/onboarding.tsx
@@ -56,7 +56,7 @@ export default function OnboardingPage() {
...registerMutation(),
onSuccess: async () => {
toast.success("Admin user created successfully!");
- navigate("/download-recovery-key");
+ void navigate("/download-recovery-key");
},
onError: (error) => {
console.error(error);
diff --git a/app/client/modules/backups/components/create-schedule-form.tsx b/app/client/modules/backups/components/create-schedule-form.tsx
index 63d3a8e0..8e2a088b 100644
--- a/app/client/modules/backups/components/create-schedule-form.tsx
+++ b/app/client/modules/backups/components/create-schedule-form.tsx
@@ -728,7 +728,7 @@ export const CreateScheduleForm = ({ initialValues, formId, onSubmit, volume }:
.filter(([key, value]) => key.startsWith("keep") && Boolean(value))
.map(([key, value]) => {
const label = key.replace("keep", "").toLowerCase();
- return `${value} ${label}`;
+ return `${value.toString()} ${label}`;
})
.join(", ") || "-"}
diff --git a/app/client/modules/backups/components/snapshot-file-browser.tsx b/app/client/modules/backups/components/snapshot-file-browser.tsx
index 6935c8e6..185b0ed3 100644
--- a/app/client/modules/backups/components/snapshot-file-browser.tsx
+++ b/app/client/modules/backups/components/snapshot-file-browser.tsx
@@ -68,7 +68,7 @@ export const SnapshotFileBrowser = (props: Props) => {
);
},
prefetchFolder: (path) => {
- queryClient.prefetchQuery(
+ void queryClient.prefetchQuery(
listSnapshotFilesOptions({
path: { id: repositoryId, snapshotId: snapshot.short_id },
query: { path },
diff --git a/app/client/modules/backups/routes/backup-details.tsx b/app/client/modules/backups/routes/backup-details.tsx
index d7b6b841..1e218979 100644
--- a/app/client/modules/backups/routes/backup-details.tsx
+++ b/app/client/modules/backups/routes/backup-details.tsx
@@ -120,7 +120,7 @@ export default function ScheduleDetailsPage({ params, loaderData }: Route.Compon
...deleteBackupScheduleMutation(),
onSuccess: () => {
toast.success("Backup schedule deleted successfully");
- navigate("/backups");
+ void navigate("/backups");
},
onError: (error) => {
toast.error("Failed to delete backup schedule", { description: parseError(error)?.message });
diff --git a/app/client/modules/backups/routes/create-backup.tsx b/app/client/modules/backups/routes/create-backup.tsx
index a7a12e1d..9fc686bb 100644
--- a/app/client/modules/backups/routes/create-backup.tsx
+++ b/app/client/modules/backups/routes/create-backup.tsx
@@ -59,7 +59,7 @@ export default function CreateBackup({ loaderData }: Route.ComponentProps) {
...createBackupScheduleMutation(),
onSuccess: (data) => {
toast.success("Backup job created successfully");
- navigate(`/backups/${data.id}`);
+ void navigate(`/backups/${data.id}`);
},
onError: (error) => {
toast.error("Failed to create backup job", {
diff --git a/app/client/modules/notifications/routes/create-notification.tsx b/app/client/modules/notifications/routes/create-notification.tsx
index 72268a6b..c6db625c 100644
--- a/app/client/modules/notifications/routes/create-notification.tsx
+++ b/app/client/modules/notifications/routes/create-notification.tsx
@@ -33,7 +33,7 @@ export default function CreateNotification() {
...createNotificationDestinationMutation(),
onSuccess: () => {
toast.success("Notification destination created successfully");
- navigate(`/notifications`);
+ void navigate(`/notifications`);
},
});
diff --git a/app/client/modules/notifications/routes/notification-details.tsx b/app/client/modules/notifications/routes/notification-details.tsx
index ba6ea340..3aae456b 100644
--- a/app/client/modules/notifications/routes/notification-details.tsx
+++ b/app/client/modules/notifications/routes/notification-details.tsx
@@ -24,7 +24,7 @@ import { getNotificationDestination } from "~/client/api-client/sdk.gen";
import type { Route } from "./+types/notification-details";
import { cn } from "~/client/lib/utils";
import { Card, CardContent, CardHeader, CardTitle } from "~/client/components/ui/card";
-import { Bell, Save, TestTube2, Trash2, X } from "lucide-react";
+import { Bell, Save, TestTube2, Trash2 } from "lucide-react";
import { Alert, AlertDescription } from "~/client/components/ui/alert";
import { CreateNotificationForm, type NotificationFormValues } from "../components/create-notification-form";
@@ -66,7 +66,7 @@ export default function NotificationDetailsPage({ loaderData }: Route.ComponentP
...deleteNotificationDestinationMutation(),
onSuccess: () => {
toast.success("Notification destination deleted successfully");
- navigate("/notifications");
+ void navigate("/notifications");
},
onError: (error) => {
toast.error("Failed to delete notification destination", {
diff --git a/app/client/modules/repositories/routes/create-repository.tsx b/app/client/modules/repositories/routes/create-repository.tsx
index 9d65b084..cad73dc4 100644
--- a/app/client/modules/repositories/routes/create-repository.tsx
+++ b/app/client/modules/repositories/routes/create-repository.tsx
@@ -36,7 +36,7 @@ export default function CreateRepository() {
...createRepositoryMutation(),
onSuccess: (data) => {
toast.success("Repository created successfully");
- navigate(`/repositories/${data.repository.shortId}`);
+ void navigate(`/repositories/${data.repository.shortId}`);
},
});
diff --git a/app/client/modules/repositories/routes/repository-details.tsx b/app/client/modules/repositories/routes/repository-details.tsx
index cfd52963..53f61a77 100644
--- a/app/client/modules/repositories/routes/repository-details.tsx
+++ b/app/client/modules/repositories/routes/repository-details.tsx
@@ -67,14 +67,14 @@ export default function RepositoryDetailsPage({ loaderData }: Route.ComponentPro
});
useEffect(() => {
- queryClient.prefetchQuery(listSnapshotsOptions({ path: { id: data.id } }));
+ void queryClient.prefetchQuery(listSnapshotsOptions({ path: { id: data.id } }));
}, [queryClient, data.id]);
const deleteRepo = useMutation({
...deleteRepositoryMutation(),
onSuccess: () => {
toast.success("Repository deleted successfully");
- navigate("/repositories");
+ void navigate("/repositories");
},
onError: (error) => {
toast.error("Failed to delete repository", {
diff --git a/app/client/modules/settings/routes/settings.tsx b/app/client/modules/settings/routes/settings.tsx
index aca29aaf..19b1f022 100644
--- a/app/client/modules/settings/routes/settings.tsx
+++ b/app/client/modules/settings/routes/settings.tsx
@@ -54,7 +54,7 @@ export default function Settings({ loaderData }: Route.ComponentProps) {
const logout = useMutation({
...logoutMutation(),
onSuccess: () => {
- navigate("/login", { replace: true });
+ void navigate("/login", { replace: true });
},
});
diff --git a/app/client/modules/volumes/routes/create-volume.tsx b/app/client/modules/volumes/routes/create-volume.tsx
index e2faf6a6..46bbec06 100644
--- a/app/client/modules/volumes/routes/create-volume.tsx
+++ b/app/client/modules/volumes/routes/create-volume.tsx
@@ -33,7 +33,7 @@ export default function CreateVolume() {
...createVolumeMutation(),
onSuccess: (data) => {
toast.success("Volume created successfully");
- navigate(`/volumes/${data.name}`);
+ void navigate(`/volumes/${data.name}`);
},
});
diff --git a/app/client/modules/volumes/routes/volume-details.tsx b/app/client/modules/volumes/routes/volume-details.tsx
index 61131a9f..f2b3da4a 100644
--- a/app/client/modules/volumes/routes/volume-details.tsx
+++ b/app/client/modules/volumes/routes/volume-details.tsx
@@ -75,7 +75,7 @@ export default function VolumeDetails({ loaderData }: Route.ComponentProps) {
...deleteVolumeMutation(),
onSuccess: () => {
toast.success("Volume deleted successfully");
- navigate("/volumes");
+ void navigate("/volumes");
},
onError: (error) => {
toast.error("Failed to delete volume", {
diff --git a/app/client/modules/volumes/tabs/info.tsx b/app/client/modules/volumes/tabs/info.tsx
index 79a9c200..c960ba6c 100644
--- a/app/client/modules/volumes/tabs/info.tsx
+++ b/app/client/modules/volumes/tabs/info.tsx
@@ -37,7 +37,7 @@ export const VolumeInfoTabContent = ({ volume, statfs }: Props) => {
setPendingValues(null);
if (data.name !== volume.name) {
- navigate(`/volumes/${data.name}`);
+ void navigate(`/volumes/${data.name}`);
}
},
onError: (error) => {
diff --git a/app/root.tsx b/app/root.tsx
index 85ac9b95..2bb3ac81 100644
--- a/app/root.tsx
+++ b/app/root.tsx
@@ -27,11 +27,11 @@ export const links: Route.LinksFunction = () => [
const queryClient = new QueryClient({
mutationCache: new MutationCache({
onSuccess: () => {
- queryClient.invalidateQueries();
+ void queryClient.invalidateQueries();
},
onError: (error) => {
console.error("Mutation error:", error);
- queryClient.invalidateQueries();
+ void queryClient.invalidateQueries();
},
}),
});
diff --git a/app/server/core/scheduler.ts b/app/server/core/scheduler.ts
index bb066c51..d115fbeb 100644
--- a/app/server/core/scheduler.ts
+++ b/app/server/core/scheduler.ts
@@ -34,7 +34,7 @@ class SchedulerClass {
async stop() {
for (const task of this.tasks) {
- task.stop();
+ await task.stop();
}
this.tasks = [];
logger.info("Scheduler stopped");
@@ -42,7 +42,7 @@ class SchedulerClass {
async clear() {
for (const task of this.tasks) {
- task.destroy();
+ await task.destroy();
}
this.tasks = [];
logger.info("Scheduler cleared all tasks");
diff --git a/app/server/index.ts b/app/server/index.ts
index 205180e0..52beea7c 100644
--- a/app/server/index.ts
+++ b/app/server/index.ts
@@ -22,9 +22,7 @@ runDbMigrations();
await retagSnapshots();
await validateRequiredMigrations(REQUIRED_MIGRATIONS);
-startup();
-
-logger.info(`Server is running at http://localhost:${config.port}`);
+await startup();
export type AppType = typeof app;
diff --git a/app/server/jobs/cleanup-sessions.ts b/app/server/jobs/cleanup-sessions.ts
index 09ec34c9..e457f1ce 100644
--- a/app/server/jobs/cleanup-sessions.ts
+++ b/app/server/jobs/cleanup-sessions.ts
@@ -3,7 +3,7 @@ import { authService } from "../modules/auth/auth.service";
export class CleanupSessionsJob extends Job {
async run() {
- authService.cleanupExpiredSessions();
+ await authService.cleanupExpiredSessions();
return { done: true, timestamp: new Date() };
}
diff --git a/app/server/modules/backups/__tests__/backups.service.test.ts b/app/server/modules/backups/__tests__/backups.service.test.ts
index 75933fc1..2c70f82e 100644
--- a/app/server/modules/backups/__tests__/backups.service.test.ts
+++ b/app/server/modules/backups/__tests__/backups.service.test.ts
@@ -101,7 +101,7 @@ describe("execute backup", () => {
});
// act
- backupsService.executeBackup(schedule.id);
+ void backupsService.executeBackup(schedule.id);
await new Promise((resolve) => setTimeout(resolve, 10));
await backupsService.executeBackup(schedule.id);
diff --git a/app/server/modules/backups/backups.service.ts b/app/server/modules/backups/backups.service.ts
index 6c29f739..d18e507b 100644
--- a/app/server/modules/backups/backups.service.ts
+++ b/app/server/modules/backups/backups.service.ts
@@ -28,7 +28,7 @@ const calculateNextRun = (cronExpression: string): number => {
return interval.next().getTime();
} catch (error) {
- logger.error(`Failed to parse cron expression "${cronExpression}": ${error}`);
+ logger.error(`Failed to parse cron expression "${cronExpression}": ${toMessage(error)}`);
const fallback = new Date();
fallback.setMinutes(fallback.getMinutes() + 1);
return fallback.getTime();
diff --git a/app/server/modules/events/events.controller.ts b/app/server/modules/events/events.controller.ts
index 4661e133..f7991735 100644
--- a/app/server/modules/events/events.controller.ts
+++ b/app/server/modules/events/events.controller.ts
@@ -13,14 +13,14 @@ export const eventsController = new Hono().use(requireAuth).get("/", (c) => {
event: "connected",
});
- const onBackupStarted = (data: { scheduleId: number; volumeName: string; repositoryName: string }) => {
- stream.writeSSE({
+ const onBackupStarted = async (data: { scheduleId: number; volumeName: string; repositoryName: string }) => {
+ await stream.writeSSE({
data: JSON.stringify(data),
event: "backup:started",
});
};
- const onBackupProgress = (data: {
+ const onBackupProgress = async (data: {
scheduleId: number;
volumeName: string;
repositoryName: string;
@@ -32,60 +32,60 @@ export const eventsController = new Hono().use(requireAuth).get("/", (c) => {
bytes_done: number;
current_files: string[];
}) => {
- stream.writeSSE({
+ await stream.writeSSE({
data: JSON.stringify(data),
event: "backup:progress",
});
};
- const onBackupCompleted = (data: {
+ const onBackupCompleted = async (data: {
scheduleId: number;
volumeName: string;
repositoryName: string;
status: "success" | "error" | "stopped" | "warning";
}) => {
- stream.writeSSE({
+ await stream.writeSSE({
data: JSON.stringify(data),
event: "backup:completed",
});
};
- const onVolumeMounted = (data: { volumeName: string }) => {
- stream.writeSSE({
+ const onVolumeMounted = async (data: { volumeName: string }) => {
+ await stream.writeSSE({
data: JSON.stringify(data),
event: "volume:mounted",
});
};
- const onVolumeUnmounted = (data: { volumeName: string }) => {
- stream.writeSSE({
+ const onVolumeUnmounted = async (data: { volumeName: string }) => {
+ await stream.writeSSE({
data: JSON.stringify(data),
event: "volume:unmounted",
});
};
- const onVolumeUpdated = (data: { volumeName: string }) => {
- stream.writeSSE({
+ const onVolumeUpdated = async (data: { volumeName: string }) => {
+ await stream.writeSSE({
data: JSON.stringify(data),
event: "volume:updated",
});
};
- const onMirrorStarted = (data: { scheduleId: number; repositoryId: string; repositoryName: string }) => {
- stream.writeSSE({
+ const onMirrorStarted = async (data: { scheduleId: number; repositoryId: string; repositoryName: string }) => {
+ await stream.writeSSE({
data: JSON.stringify(data),
event: "mirror:started",
});
};
- const onMirrorCompleted = (data: {
+ const onMirrorCompleted = async (data: {
scheduleId: number;
repositoryId: string;
repositoryName: string;
status: "success" | "error";
error?: string;
}) => {
- stream.writeSSE({
+ await stream.writeSSE({
data: JSON.stringify(data),
event: "mirror:completed",
});
diff --git a/app/server/utils/logger.ts b/app/server/utils/logger.ts
index ecc323b2..6d106f60 100644
--- a/app/server/utils/logger.ts
+++ b/app/server/utils/logger.ts
@@ -3,7 +3,7 @@ import { sanitizeSensitiveData } from "./sanitize";
const { printf, combine, colorize } = format;
-const printConsole = printf((info) => `${info.level} > ${info.message}`);
+const printConsole = printf((info) => `${info.level} > ${String(info.message)}`);
const consoleFormat = combine(colorize(), printConsole);
const getDefaultLevel = () => {
@@ -27,7 +27,7 @@ const log = (level: "info" | "warn" | "error" | "debug", messages: unknown[]) =>
return sanitizeSensitiveData(JSON.stringify(m, null, 2));
}
- return sanitizeSensitiveData(String(m));
+ return sanitizeSensitiveData(String(JSON.stringify(m)));
});
winstonLogger.log(level, stringMessages.join(" "));
diff --git a/app/server/utils/rclone.ts b/app/server/utils/rclone.ts
index 92beeccc..4e6470d0 100644
--- a/app/server/utils/rclone.ts
+++ b/app/server/utils/rclone.ts
@@ -1,5 +1,6 @@
import { $ } from "bun";
import { logger } from "./logger";
+import { toMessage } from "./errors";
/**
* List all configured rclone remotes
@@ -9,7 +10,7 @@ export async function listRcloneRemotes(): Promise {
const result = await $`rclone listremotes`.nothrow();
if (result.exitCode !== 0) {
- logger.error(`Failed to list rclone remotes: ${result.stderr}`);
+ logger.error(`Failed to list rclone remotes: ${result.stderr.toString()}`);
return [];
}
@@ -36,7 +37,7 @@ export async function getRcloneRemoteInfo(
const result = await $`rclone config show ${remote}`.quiet();
if (result.exitCode !== 0) {
- logger.error(`Failed to get info for remote ${remote}: ${result.stderr}`);
+ logger.error(`Failed to get info for remote ${remote}: ${result.stderr.toString()}`);
return null;
}
@@ -70,7 +71,7 @@ export async function getRcloneRemoteInfo(
return { type, config };
} catch (error) {
- logger.error(`Error getting remote info for ${remote}: ${error}`);
+ logger.error(`Error getting remote info for ${remote}: ${toMessage(error)}`);
return null;
}
}
diff --git a/app/server/utils/restic.ts b/app/server/utils/restic.ts
index 17d05310..1e2051ab 100644
--- a/app/server/utils/restic.ts
+++ b/app/server/utils/restic.ts
@@ -358,8 +358,8 @@ const backup = async (
}
},
finally: async () => {
- includeFile && (await fs.unlink(includeFile).catch(() => {}));
- excludeFile && (await fs.unlink(excludeFile).catch(() => {}));
+ if (includeFile) await fs.unlink(includeFile).catch(() => {});
+ if (excludeFile) await fs.unlink(excludeFile).catch(() => {});
await cleanupTemporaryKeys(env);
},
});
@@ -393,7 +393,7 @@ const backup = async (
const result = backupOutputSchema(summaryLine);
if (result instanceof type.errors) {
- logger.error(`Restic backup output validation failed: ${result}`);
+ logger.error(`Restic backup output validation failed: ${result.summary}`);
return { result: null, exitCode: res.exitCode };
}
@@ -484,7 +484,7 @@ const restore = async (
const result = restoreOutputSchema(resSummary);
if (result instanceof type.errors) {
- logger.warn(`Restic restore output validation failed: ${result}`);
+ logger.warn(`Restic restore output validation failed: ${result.summary}`);
logger.info(`Restic restore completed for snapshot ${snapshotId} to target ${target}`);
return {
message_type: "summary" as const,
@@ -529,8 +529,8 @@ const snapshots = async (config: RepositoryConfig, options: { tags?: string[] }
const result = snapshotInfoSchema.array()(JSON.parse(res.stdout));
if (result instanceof type.errors) {
- logger.error(`Restic snapshots output validation failed: ${result}`);
- throw new Error(`Restic snapshots output validation failed: ${result}`);
+ logger.error(`Restic snapshots output validation failed: ${result.summary}`);
+ throw new Error(`Restic snapshots output validation failed: ${result.summary}`);
}
return result;
@@ -712,8 +712,8 @@ const ls = async (config: RepositoryConfig, snapshotId: string, path?: string) =
const snapshot = lsSnapshotInfoSchema(snapshotLine);
if (snapshot instanceof type.errors) {
- logger.error(`Restic ls snapshot info validation failed: ${snapshot}`);
- throw new Error(`Restic ls snapshot info validation failed: ${snapshot}`);
+ logger.error(`Restic ls snapshot info validation failed: ${snapshot.summary}`);
+ throw new Error(`Restic ls snapshot info validation failed: ${snapshot.summary}`);
}
const nodes: Array = [];
@@ -722,7 +722,7 @@ const ls = async (config: RepositoryConfig, snapshotId: string, path?: string) =
const nodeValidation = lsNodeSchema(nodeLine);
if (nodeValidation instanceof type.errors) {
- logger.warn(`Skipping invalid node: ${nodeValidation}`);
+ logger.warn(`Skipping invalid node: ${nodeValidation.summary}`);
continue;
}
diff --git a/app/test/setup.ts b/app/test/setup.ts
index 8791d42c..43360902 100644
--- a/app/test/setup.ts
+++ b/app/test/setup.ts
@@ -4,7 +4,7 @@ import path from "node:path";
import { cwd } from "node:process";
import { db } from "~/server/db/db";
-mock.module("~/server/utils/logger", () => ({
+void mock.module("~/server/utils/logger", () => ({
logger: {
debug: () => {},
info: () => {},
diff --git a/biome.json b/biome.json
deleted file mode 100644
index 602097c3..00000000
--- a/biome.json
+++ /dev/null
@@ -1,43 +0,0 @@
-{
- "$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
- "vcs": {
- "enabled": true,
- "clientKind": "git",
- "defaultBranch": "origin/main",
- "useIgnoreFile": true
- },
- "files": {
- "includes": ["**/*.{ts,tsx,json}", "!**/api-client", "!**/components/ui"],
- "ignoreUnknown": false
- },
- "formatter": {
- "enabled": true,
- "indentStyle": "tab",
- "lineWidth": 120,
- "lineEnding": "lf"
- },
- "linter": {
- "enabled": true,
- "rules": {
- "recommended": true
- }
- },
- "javascript": {
- "formatter": {
- "quoteStyle": "double"
- }
- },
- "assist": {
- "enabled": true,
- "actions": {
- "source": {
- "organizeImports": "off"
- }
- }
- },
- "css": {
- "parser": {
- "tailwindDirectives": true
- }
- }
-}
diff --git a/bun.lock b/bun.lock
index d69df99c..b7fb5e1b 100644
--- a/bun.lock
+++ b/bun.lock
@@ -64,7 +64,6 @@
"yaml": "^2.8.2",
},
"devDependencies": {
- "@biomejs/biome": "^2.3.8",
"@faker-js/faker": "^10.1.0",
"@happy-dom/global-registrator": "^20.0.11",
"@hey-api/openapi-ts": "^0.88.0",
@@ -82,6 +81,9 @@
"dotenv-cli": "^11.0.0",
"drizzle-kit": "^0.31.7",
"lightningcss": "^1.30.2",
+ "oxfmt": "^0.22.0",
+ "oxlint": "^1.36.0",
+ "oxlint-tsgolint": "^0.10.1",
"tailwindcss": "^4.1.17",
"tinyglobby": "^0.2.15",
"tw-animate-css": "^1.4.0",
@@ -158,24 +160,6 @@
"@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
- "@biomejs/biome": ["@biomejs/biome@2.3.10", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.10", "@biomejs/cli-darwin-x64": "2.3.10", "@biomejs/cli-linux-arm64": "2.3.10", "@biomejs/cli-linux-arm64-musl": "2.3.10", "@biomejs/cli-linux-x64": "2.3.10", "@biomejs/cli-linux-x64-musl": "2.3.10", "@biomejs/cli-win32-arm64": "2.3.10", "@biomejs/cli-win32-x64": "2.3.10" }, "bin": { "biome": "bin/biome" } }, "sha512-/uWSUd1MHX2fjqNLHNL6zLYWBbrJeG412/8H7ESuK8ewoRoMPUgHDebqKrPTx/5n6f17Xzqc9hdg3MEqA5hXnQ=="],
-
- "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-M6xUjtCVnNGFfK7HMNKa593nb7fwNm43fq1Mt71kpLpb+4mE7odO8W/oWVDyBVO4ackhresy1ZYO7OJcVo/B7w=="],
-
- "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.3.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-Vae7+V6t/Avr8tVbFNjnFSTKZogZHFYl7MMH62P/J1kZtr0tyRQ9Fe0onjqjS2Ek9lmNLmZc/VR5uSekh+p1fg=="],
-
- "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.3.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-hhPw2V3/EpHKsileVOFynuWiKRgFEV48cLe0eA+G2wO4SzlwEhLEB9LhlSrVeu2mtSn205W283LkX7Fh48CaxA=="],
-
- "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.3.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-B9DszIHkuKtOH2IFeeVkQmSMVUjss9KtHaNXquYYWCjH8IstNgXgx5B0aSBQNr6mn4RcKKRQZXn9Zu1rM3O0/A=="],
-
- "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.3.10", "", { "os": "linux", "cpu": "x64" }, "sha512-wwAkWD1MR95u+J4LkWP74/vGz+tRrIQvr8kfMMJY8KOQ8+HMVleREOcPYsQX82S7uueco60L58Wc6M1I9WA9Dw=="],
-
- "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.3.10", "", { "os": "linux", "cpu": "x64" }, "sha512-QTfHZQh62SDFdYc2nfmZFuTm5yYb4eO1zwfB+90YxUumRCR171tS1GoTX5OD0wrv4UsziMPmrePMtkTnNyYG3g=="],
-
- "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.3.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-o7lYc9n+CfRbHvkjPhm8s9FgbKdYZu5HCcGVMItLjz93EhgJ8AM44W+QckDqLA9MKDNFrR8nPbO4b73VC5kGGQ=="],
-
- "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.10", "", { "os": "win32", "cpu": "x64" }, "sha512-pHEFgq7dUEsKnqG9mx9bXihxGI49X+ar+UBrEIj3Wqj3UCZp1rNgV+OoyjFgcXsjCWpuEAF4VJdkZr3TrWdCbQ=="],
-
"@colors/colors": ["@colors/colors@1.6.0", "", {}, "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA=="],
"@dabh/diagnostics": ["@dabh/diagnostics@2.0.8", "", { "dependencies": { "@so-ric/colorspace": "^1.1.6", "enabled": "2.0.x", "kuler": "^2.0.0" } }, "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q=="],
@@ -320,6 +304,50 @@
"@mjackson/node-fetch-server": ["@mjackson/node-fetch-server@0.2.0", "", {}, "sha512-EMlH1e30yzmTpGLQjlFmaDAjyOeZhng1/XCd7DExR8PNAnG/G1tyruZxEoUe11ClnwGhGrtsdnyyUx1frSzjng=="],
+ "@oxfmt/darwin-arm64": ["@oxfmt/darwin-arm64@0.22.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-dhz2m2uLrHT3MwM+LAdvr97EojJZTwaZ6BuMTRftJzqa9dHYDG/MtSBuDD2DpGpZ0SM2iVwni2wCzCYGKTojbA=="],
+
+ "@oxfmt/darwin-x64": ["@oxfmt/darwin-x64@0.22.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-VykUbibvqSOG5YIFUMpHtZVrY1YKDl9Il2SvFemUfR5Ac1t1BFZOnazYe98jtZGFY4sEdEORs0ImBARnyMX/hw=="],
+
+ "@oxfmt/linux-arm64-gnu": ["@oxfmt/linux-arm64-gnu@0.22.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-y0MBha/K34TztYAZUn6KQE9xLPLNHqRpOdzRp96fhkbrQTeEXo+jF+8+aV8VnqjG0y7p+IQN4ATxNSstSPO9sA=="],
+
+ "@oxfmt/linux-arm64-musl": ["@oxfmt/linux-arm64-musl@0.22.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-8a0p2UEmavB+moQ7ID17i+dE7N2xng6lPU8vrrNnnwKde0YpGHdW6hmuH4mS+rrltvs0fjyGRSvCnD2Qm9IAcA=="],
+
+ "@oxfmt/linux-x64-gnu": ["@oxfmt/linux-x64-gnu@0.22.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ZA1lS6MLvtGfD9AaDylCSTTiOWVQs1eIl9uqsGYs+Zr8p0mI7QRIRA6juWk9FXn1hHfmYBdBgWu2GdIW0YFCFA=="],
+
+ "@oxfmt/linux-x64-musl": ["@oxfmt/linux-x64-musl@0.22.0", "", { "os": "linux", "cpu": "x64" }, "sha512-J5zFB8T5yk6Jx63rdKuXfcPqR1cAp12nO5/NJfGITH00AML2Yj9JM4dRnmssJomYHKa8dNSr40l6OdxRZN88CQ=="],
+
+ "@oxfmt/win32-arm64": ["@oxfmt/win32-arm64@0.22.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-XYxyIiOf3HqlfETLFKqCHYL88mhw+Ka25vDVgmlcghbJv9BPoVzquZW7P4i0T3D5GWp4LHhZHmMo8BuK8PP5BA=="],
+
+ "@oxfmt/win32-x64": ["@oxfmt/win32-x64@0.22.0", "", { "os": "win32", "cpu": "x64" }, "sha512-/shfU+wwlXcKP2NkZt+kYCSVom2EEu8MwbENlYCak6LtPPrN5xAQhHuOSFByjDzTBApdQugch0j0ZB/4Wyaljg=="],
+
+ "@oxlint-tsgolint/darwin-arm64": ["@oxlint-tsgolint/darwin-arm64@0.10.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-KGC4++BeEqrIcmDHiJt/e6/860PWJmUJjjp0mE+smpBmRXMjmOFFjrPmN+ZyCyVgf1WdmhPkQXsRSPeTR+2omw=="],
+
+ "@oxlint-tsgolint/darwin-x64": ["@oxlint-tsgolint/darwin-x64@0.10.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-tvmrDgj3Q0tdc+zMWfCVLVq8EQDEUqasm1zaWgSMYIszpID6qdgqbT+OpWWXV9fLZgtvrkoXGwxkHAUJzdVZXQ=="],
+
+ "@oxlint-tsgolint/linux-arm64": ["@oxlint-tsgolint/linux-arm64@0.10.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-7kD28z6/ykGx8WetKTPRZt30pd+ziassxg/8cM24lhjUI+hNXyRHVtHes73dh9D6glJKno+1ut+3amUdZBZcpQ=="],
+
+ "@oxlint-tsgolint/linux-x64": ["@oxlint-tsgolint/linux-x64@0.10.1", "", { "os": "linux", "cpu": "x64" }, "sha512-NmJmiqdzYUTHIxteSTyX6IFFgnIsOAjRWXfrS6Jbo5xlB3g39WHniSF3asB/khLJNtwSg4InUS34NprYM7zrEw=="],
+
+ "@oxlint-tsgolint/win32-arm64": ["@oxlint-tsgolint/win32-arm64@0.10.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-3KrT80vl3nXUkjuJI/z8dF6xWsKx0t9Tz4ZQHgQw3fYw+CoihBRWGklrdlmCz+EGfMyVaQLqBV9PZckhSqLe2A=="],
+
+ "@oxlint-tsgolint/win32-x64": ["@oxlint-tsgolint/win32-x64@0.10.1", "", { "os": "win32", "cpu": "x64" }, "sha512-hW1fSJZVxG51sLdGq1sQjOzb1tsQ23z/BquJfUwL7CqBobxr7TJvGmoINL+9KryOJt0jCoaiMfWe4yoYw5XfIA=="],
+
+ "@oxlint/darwin-arm64": ["@oxlint/darwin-arm64@1.36.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-MJkj82GH+nhvWKJhSIM6KlZ8tyGKdogSQXtNdpIyP02r/tTayFJQaAEWayG2Jhsn93kske+nimg5MYFhwO/rlg=="],
+
+ "@oxlint/darwin-x64": ["@oxlint/darwin-x64@1.36.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-VvEhfkqj/99dCTqOcfkyFXOSbx4lIy5u2m2GHbK4WCMDySokOcMTNRHGw8fH/WgQ5cDrDMSTYIGQTmnBGi9tiQ=="],
+
+ "@oxlint/linux-arm64-gnu": ["@oxlint/linux-arm64-gnu@1.36.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-EMx92X5q+hHc3olTuj/kgkx9+yP0p/AVs4yvHbUfzZhBekXNpUWxWvg4hIKmQWn+Ee2j4o80/0ACGO0hDYJ9mg=="],
+
+ "@oxlint/linux-arm64-musl": ["@oxlint/linux-arm64-musl@1.36.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-7YCxtrPIctVYLqWrWkk8pahdCxch6PtsaucfMLC7TOlDt4nODhnQd4yzEscKqJ8Gjrw1bF4g+Ngob1gB+Qr9Fw=="],
+
+ "@oxlint/linux-x64-gnu": ["@oxlint/linux-x64-gnu@1.36.0", "", { "os": "linux", "cpu": "x64" }, "sha512-lnaJVlx5r3NWmoOMesfQXJSf78jHTn8Z+sdAf795Kgteo72+qGC1Uax2SToCJVN2J8PNG3oRV5bLriiCNR2i6Q=="],
+
+ "@oxlint/linux-x64-musl": ["@oxlint/linux-x64-musl@1.36.0", "", { "os": "linux", "cpu": "x64" }, "sha512-AhuEU2Qdl66lSfTGu/Htirq8r/8q2YnZoG3yEXLMQWnPMn7efy8spD/N1NA7kH0Hll+cdfwgQkQqC2G4MS2lPQ=="],
+
+ "@oxlint/win32-arm64": ["@oxlint/win32-arm64@1.36.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-GlWCBjUJY2QgvBFuNRkiRJu7K/djLmM0UQKfZV8IN+UXbP/JbjZHWKRdd4LXlQmzoz7M5Hd6p+ElCej8/90FCg=="],
+
+ "@oxlint/win32-x64": ["@oxlint/win32-x64@1.36.0", "", { "os": "win32", "cpu": "x64" }, "sha512-J+Vc00Utcf8p77lZPruQgb0QnQXuKnFogN88kCnOqs2a83I+vTBB8ILr0+L9sTwVRvIDMSC0pLdeQH4svWGFZg=="],
+
"@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="],
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
@@ -1112,6 +1140,12 @@
"openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="],
+ "oxfmt": ["oxfmt@0.22.0", "", { "dependencies": { "tinypool": "2.0.0" }, "optionalDependencies": { "@oxfmt/darwin-arm64": "0.22.0", "@oxfmt/darwin-x64": "0.22.0", "@oxfmt/linux-arm64-gnu": "0.22.0", "@oxfmt/linux-arm64-musl": "0.22.0", "@oxfmt/linux-x64-gnu": "0.22.0", "@oxfmt/linux-x64-musl": "0.22.0", "@oxfmt/win32-arm64": "0.22.0", "@oxfmt/win32-x64": "0.22.0" }, "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-Z7JM5yv4KaDz5kT21MxRAvtYo5Eu9Ti/XY1JYShSOlaH859XSn4UaS3wlZKyz4Mpbo8ISkxhU75UY5yp+OQUyA=="],
+
+ "oxlint": ["oxlint@1.36.0", "", { "optionalDependencies": { "@oxlint/darwin-arm64": "1.36.0", "@oxlint/darwin-x64": "1.36.0", "@oxlint/linux-arm64-gnu": "1.36.0", "@oxlint/linux-arm64-musl": "1.36.0", "@oxlint/linux-x64-gnu": "1.36.0", "@oxlint/linux-x64-musl": "1.36.0", "@oxlint/win32-arm64": "1.36.0", "@oxlint/win32-x64": "1.36.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.10.0" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxc_language_server": "bin/oxc_language_server", "oxlint": "bin/oxlint" } }, "sha512-IicUdXfXgI8OKrDPnoSjvBfeEF8PkKtm+CoLlg4LYe4ypc8U+T4r7730XYshdBGZdelg+JRw8GtCb2w/KaaZvw=="],
+
+ "oxlint-tsgolint": ["oxlint-tsgolint@0.10.1", "", { "optionalDependencies": { "@oxlint-tsgolint/darwin-arm64": "0.10.1", "@oxlint-tsgolint/darwin-x64": "0.10.1", "@oxlint-tsgolint/linux-arm64": "0.10.1", "@oxlint-tsgolint/linux-x64": "0.10.1", "@oxlint-tsgolint/win32-arm64": "0.10.1", "@oxlint-tsgolint/win32-x64": "0.10.1" }, "bin": { "tsgolint": "bin/tsgolint.js" } }, "sha512-EEHNdo5cW2w1xwYdBQ7d3IXDqWAtMkfVFrh+9gQ4kYbYJwygY4QXSh1eH80/xVipZdVKujAwBgg/nNNHk56kxQ=="],
+
"p-map": ["p-map@7.0.4", "", {}, "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ=="],
"parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="],
@@ -1286,6 +1320,8 @@
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
+ "tinypool": ["tinypool@2.0.0", "", {}, "sha512-/RX9RzeH2xU5ADE7n2Ykvmi9ED3FBGPAjw9u3zucrNNaEBIO0HPSYgL0NT7+3p147ojeSdaVu08F6hjpv31HJg=="],
+
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
"trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
diff --git a/components.json b/components.json
index e74bbe43..94cd1628 100644
--- a/components.json
+++ b/components.json
@@ -11,11 +11,11 @@
"prefix": ""
},
"aliases": {
- "components": "~/components",
- "utils": "~/lib/utils",
- "ui": "~/components/ui",
- "lib": "~/lib",
- "hooks": "~/hooks"
+ "components": "~/client/components",
+ "utils": "~/client/lib/utils",
+ "ui": "~/client/components/ui",
+ "lib": "~/client/lib",
+ "hooks": "~/client/hooks"
},
"iconLibrary": "lucide"
}
diff --git a/package.json b/package.json
index fb4586b3..f13d45de 100644
--- a/package.json
+++ b/package.json
@@ -4,14 +4,13 @@
"type": "module",
"packageManager": "bun@1.3.5",
"scripts": {
+ "lint": "oxlint --type-aware",
"build": "react-router build",
"dev": "bunx --bun vite",
"start": "bun ./dist/server/index.js",
"cli:dev": "bun run app/server/cli/main.ts",
"cli": "ZEROBYTE_CLI=1 bun ./dist/server/index.js",
"tsc": "react-router typegen && tsc",
- "lint": "biome check .",
- "lint:ci": "biome ci . --changed --error-on-warnings --no-errors-on-unmatched",
"start:dev": "docker compose down && docker compose up --build zerobyte-dev",
"start:prod": "docker compose down && docker compose up --build zerobyte-prod",
"gen:api-client": "openapi-ts",
@@ -84,7 +83,6 @@
"yaml": "^2.8.2"
},
"devDependencies": {
- "@biomejs/biome": "^2.3.8",
"@faker-js/faker": "^10.1.0",
"@happy-dom/global-registrator": "^20.0.11",
"@hey-api/openapi-ts": "^0.88.0",
@@ -102,6 +100,9 @@
"dotenv-cli": "^11.0.0",
"drizzle-kit": "^0.31.7",
"lightningcss": "^1.30.2",
+ "oxfmt": "^0.22.0",
+ "oxlint": "^1.36.0",
+ "oxlint-tsgolint": "^0.10.1",
"tailwindcss": "^4.1.17",
"tinyglobby": "^0.2.15",
"tw-animate-css": "^1.4.0",
diff --git a/tsconfig.json b/tsconfig.json
index 49baa089..4967a03c 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -8,7 +8,6 @@
"moduleResolution": "bundler",
"jsx": "react-jsx",
"rootDirs": [".", "./.react-router/types"],
- "baseUrl": ".",
"paths": {
"~/*": ["./app/*"]
},
From c9dc6631042a4ea1805dfd4d9322e16f81846ed2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jakub=20Tr=C3=A1vn=C3=ADk?=
Date: Tue, 23 Dec 2025 12:23:01 +0100
Subject: [PATCH 06/11] feat(ui): improve local repository path display and
messaging
- Add shared client constants file with REPOSITORY_BASE
- Show effective local path in repository info tab
- Update host mount warning to clarify default path is safe when using recommended Docker setup
- Update README to clarify local repositories can use custom paths
---
README.md | 2 +-
app/client/lib/constants.ts | 2 ++
.../repository-forms/local-repository-form.tsx | 9 +++++----
app/client/modules/repositories/tabs/info.tsx | 14 ++++++++++++++
4 files changed, 22 insertions(+), 5 deletions(-)
create mode 100644 app/client/lib/constants.ts
diff --git a/README.md b/README.md
index 2b91dfb8..c48914ce 100644
--- a/README.md
+++ b/README.md
@@ -170,7 +170,7 @@ Now, when adding a new volume in the Zerobyte web interface, you can select "Dir
A repository is where your backups will be securely stored encrypted. Zerobyte supports multiple storage backends for your backup repositories:
-- **Local directories** - Store backups on local disk at `/var/lib/zerobyte/repositories/`
+- **Local directories** - Store backups on local disk subfolder of `/var/lib/zerobyte/repositories/` or any other (mounted) path
- **S3-compatible storage** - Amazon S3, MinIO, Wasabi, DigitalOcean Spaces, etc.
- **Google Cloud Storage** - Google's cloud storage service
- **Azure Blob Storage** - Microsoft Azure storage
diff --git a/app/client/lib/constants.ts b/app/client/lib/constants.ts
new file mode 100644
index 00000000..84464025
--- /dev/null
+++ b/app/client/lib/constants.ts
@@ -0,0 +1,2 @@
+/** Default base path for local repositories */
+export const REPOSITORY_BASE = "/var/lib/zerobyte/repositories";
diff --git a/app/client/modules/repositories/components/repository-forms/local-repository-form.tsx b/app/client/modules/repositories/components/repository-forms/local-repository-form.tsx
index e0f5b4c1..5a31755c 100644
--- a/app/client/modules/repositories/components/repository-forms/local-repository-form.tsx
+++ b/app/client/modules/repositories/components/repository-forms/local-repository-form.tsx
@@ -1,6 +1,7 @@
import { useState } from "react";
import type { UseFormReturn } from "react-hook-form";
import { Check, Pencil, X, AlertTriangle } from "lucide-react";
+import { REPOSITORY_BASE } from "~/client/lib/constants";
import { Button } from "../../../../components/ui/button";
import { FormItem, FormLabel, FormDescription } from "../../../../components/ui/form";
import { DirectoryBrowser } from "../../../../components/directory-browser";
@@ -30,7 +31,7 @@ export const LocalRepositoryForm = ({ form }: Props) => {
Repository Directory
- {form.watch("path") || "/var/lib/zerobyte/repositories"}
+ {form.watch("path") || REPOSITORY_BASE}
setShowPathWarning(true)} size="sm">
@@ -53,8 +54,8 @@ export const LocalRepositoryForm = ({ form }: Props) => {
If the path is not a host mount, you will lose your repository data when the container restarts.
- The default path /var/lib/zerobyte/repositories is
- already mounted from the host and is safe to use.
+ The default path {REPOSITORY_BASE} is
+ safe to use if you followed the recommended Docker Compose setup.
@@ -83,7 +84,7 @@ export const LocalRepositoryForm = ({ form }: Props) => {
form.setValue("path", path)}
- selectedPath={form.watch("path") || "/var/lib/zerobyte/repositories"}
+ selectedPath={form.watch("path") || REPOSITORY_BASE}
/>
diff --git a/app/client/modules/repositories/tabs/info.tsx b/app/client/modules/repositories/tabs/info.tsx
index 2b62871a..7100fca2 100644
--- a/app/client/modules/repositories/tabs/info.tsx
+++ b/app/client/modules/repositories/tabs/info.tsx
@@ -18,6 +18,7 @@ import {
AlertDialogTitle,
} from "~/client/components/ui/alert-dialog";
import type { Repository } from "~/client/lib/types";
+import { REPOSITORY_BASE } from "~/client/lib/constants";
import { updateRepositoryMutation } from "~/client/api-client/@tanstack/react-query.gen";
import type { CompressionMode, RepositoryConfig } from "~/schemas/restic";
@@ -25,6 +26,13 @@ type Props = {
repository: Repository;
};
+const getEffectiveLocalPath = (repository: Repository): string | null => {
+ if (repository.type !== "local") return null;
+ const config = repository.config as { name: string; path?: string };
+ const basePath = config.path || REPOSITORY_BASE;
+ return `${basePath}/${config.name}`;
+};
+
export const RepositoryInfoTabContent = ({ repository }: Props) => {
const [name, setName] = useState(repository.name);
const [compressionMode, setCompressionMode] = useState(
@@ -108,6 +116,12 @@ export const RepositoryInfoTabContent = ({ repository }: Props) => {
Status
{repository.status || "unknown"}
+ {getEffectiveLocalPath(repository) && (
+
+
Effective Local Path
+
{getEffectiveLocalPath(repository)}
+
+ )}
Created at
{new Date(repository.createdAt).toLocaleString()}
From 4beb521c52148166af7c08444122aacfba26b363 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jakub=20Tr=C3=A1vn=C3=ADk?=
Date: Tue, 23 Dec 2025 12:37:12 +0100
Subject: [PATCH 07/11] fix(info): optimize effective local path retrieval in
RepositoryInfoTabContent
---
app/client/modules/repositories/tabs/info.tsx | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/app/client/modules/repositories/tabs/info.tsx b/app/client/modules/repositories/tabs/info.tsx
index 7100fca2..25308f82 100644
--- a/app/client/modules/repositories/tabs/info.tsx
+++ b/app/client/modules/repositories/tabs/info.tsx
@@ -40,6 +40,9 @@ export const RepositoryInfoTabContent = ({ repository }: Props) => {
);
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
+ const isImportedLocal = repository.type === "local" && repository.config.isExistingRepository;
+ const effectiveLocalPath = getEffectiveLocalPath(repository);
+
const updateMutation = useMutation({
...updateRepositoryMutation(),
onSuccess: () => {
@@ -116,10 +119,10 @@ export const RepositoryInfoTabContent = ({ repository }: Props) => {
Status
{repository.status || "unknown"}
- {getEffectiveLocalPath(repository) && (
+ {effectiveLocalPath && (
Effective Local Path
-
{getEffectiveLocalPath(repository)}
+
{effectiveLocalPath}
)}
From 61aaa8938ab8b3b4db451be9f458dd71081ad3c7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jakub=20Tr=C3=A1vn=C3=ADk?=
Date: Sat, 3 Jan 2026 12:47:03 +0100
Subject: [PATCH 08/11] fix: handle imported repositories correctly in
getEffectiveLocalPath
- Imported repositories use config.path directly (matching server logic)
- Removed unused isImportedLocal variable
---
.../repository-forms/local-repository-form.tsx | 4 ++--
app/client/modules/repositories/tabs/info.tsx | 10 ++++++++--
2 files changed, 10 insertions(+), 4 deletions(-)
diff --git a/app/client/modules/repositories/components/repository-forms/local-repository-form.tsx b/app/client/modules/repositories/components/repository-forms/local-repository-form.tsx
index 5a31755c..ecc1122b 100644
--- a/app/client/modules/repositories/components/repository-forms/local-repository-form.tsx
+++ b/app/client/modules/repositories/components/repository-forms/local-repository-form.tsx
@@ -54,8 +54,8 @@ export const LocalRepositoryForm = ({ form }: Props) => {
If the path is not a host mount, you will lose your repository data when the container restarts.
- The default path {REPOSITORY_BASE} is
- safe to use if you followed the recommended Docker Compose setup.
+ The default path {REPOSITORY_BASE} is safe to use if you
+ followed the recommended Docker Compose setup.
diff --git a/app/client/modules/repositories/tabs/info.tsx b/app/client/modules/repositories/tabs/info.tsx
index 25308f82..fea1e191 100644
--- a/app/client/modules/repositories/tabs/info.tsx
+++ b/app/client/modules/repositories/tabs/info.tsx
@@ -28,7 +28,14 @@ type Props = {
const getEffectiveLocalPath = (repository: Repository): string | null => {
if (repository.type !== "local") return null;
- const config = repository.config as { name: string; path?: string };
+ const config = repository.config as { name: string; path?: string; isExistingRepository?: boolean };
+
+ if (config.isExistingRepository) {
+ // Imported repositories use the path directly
+ return config.path ?? null;
+ }
+
+ // New repositories append the name to the base path
const basePath = config.path || REPOSITORY_BASE;
return `${basePath}/${config.name}`;
};
@@ -40,7 +47,6 @@ export const RepositoryInfoTabContent = ({ repository }: Props) => {
);
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
- const isImportedLocal = repository.type === "local" && repository.config.isExistingRepository;
const effectiveLocalPath = getEffectiveLocalPath(repository);
const updateMutation = useMutation({
From 13428224e8e74bebf49aecd08886f5202b27d8b9 Mon Sep 17 00:00:00 2001
From: Nicolas Meienberger
Date: Mon, 5 Jan 2026 21:06:36 +0100
Subject: [PATCH 09/11] chore: remove needless comments
---
app/client/lib/constants.ts | 1 -
app/client/modules/repositories/tabs/info.tsx | 2 --
2 files changed, 3 deletions(-)
diff --git a/app/client/lib/constants.ts b/app/client/lib/constants.ts
index 84464025..801c5768 100644
--- a/app/client/lib/constants.ts
+++ b/app/client/lib/constants.ts
@@ -1,2 +1 @@
-/** Default base path for local repositories */
export const REPOSITORY_BASE = "/var/lib/zerobyte/repositories";
diff --git a/app/client/modules/repositories/tabs/info.tsx b/app/client/modules/repositories/tabs/info.tsx
index fea1e191..e22b8c6a 100644
--- a/app/client/modules/repositories/tabs/info.tsx
+++ b/app/client/modules/repositories/tabs/info.tsx
@@ -31,11 +31,9 @@ const getEffectiveLocalPath = (repository: Repository): string | null => {
const config = repository.config as { name: string; path?: string; isExistingRepository?: boolean };
if (config.isExistingRepository) {
- // Imported repositories use the path directly
return config.path ?? null;
}
- // New repositories append the name to the base path
const basePath = config.path || REPOSITORY_BASE;
return `${basePath}/${config.name}`;
};
From f0e956783ad57d4dec02e611ed497bcb40ba8ac3 Mon Sep 17 00:00:00 2001
From: Nico <47644445+nicotsx@users.noreply.github.com>
Date: Mon, 5 Jan 2026 21:10:00 +0100
Subject: [PATCH 10/11] refactor: improve perf by calling api in parallel in
clientLoaders (#312)
* refactor: improve perf by calling api in parallel in clientLoaders
* fix: dependent api calls
---
.../components/schedule-mirrors-config.tsx | 5 ++-
.../schedule-notifications-config.tsx | 5 ++-
.../modules/backups/routes/backup-details.tsx | 33 +++++++++++++++----
.../modules/backups/routes/create-backup.tsx | 3 +-
.../backups/routes/restore-snapshot.tsx | 17 ++++++----
.../repositories/routes/restore-snapshot.tsx | 10 +++---
.../repositories/routes/snapshot-details.tsx | 11 ++++---
7 files changed, 59 insertions(+), 25 deletions(-)
diff --git a/app/client/modules/backups/components/schedule-mirrors-config.tsx b/app/client/modules/backups/components/schedule-mirrors-config.tsx
index 725d3969..52879c77 100644
--- a/app/client/modules/backups/components/schedule-mirrors-config.tsx
+++ b/app/client/modules/backups/components/schedule-mirrors-config.tsx
@@ -21,11 +21,13 @@ import { StatusDot } from "~/client/components/status-dot";
import { formatDistanceToNow } from "date-fns";
import { Link } from "react-router";
import { cn } from "~/client/lib/utils";
+import type { GetScheduleMirrorsResponse } from "~/client/api-client";
type Props = {
scheduleId: number;
primaryRepositoryId: string;
repositories: Repository[];
+ initialData: GetScheduleMirrorsResponse;
};
type MirrorAssignment = {
@@ -36,13 +38,14 @@ type MirrorAssignment = {
lastCopyError: string | null;
};
-export const ScheduleMirrorsConfig = ({ scheduleId, primaryRepositoryId, repositories }: Props) => {
+export const ScheduleMirrorsConfig = ({ scheduleId, primaryRepositoryId, repositories, initialData }: Props) => {
const [assignments, setAssignments] = useState>(new Map());
const [hasChanges, setHasChanges] = useState(false);
const [isAddingNew, setIsAddingNew] = useState(false);
const { data: currentMirrors } = useQuery({
...getScheduleMirrorsOptions({ path: { scheduleId: scheduleId.toString() } }),
+ initialData,
});
const { data: compatibility } = useQuery({
diff --git a/app/client/modules/backups/components/schedule-notifications-config.tsx b/app/client/modules/backups/components/schedule-notifications-config.tsx
index 0a1d4f37..11a843f1 100644
--- a/app/client/modules/backups/components/schedule-notifications-config.tsx
+++ b/app/client/modules/backups/components/schedule-notifications-config.tsx
@@ -14,10 +14,12 @@ import {
} from "~/client/api-client/@tanstack/react-query.gen";
import { parseError } from "~/client/lib/errors";
import type { NotificationDestination } from "~/client/lib/types";
+import type { GetScheduleNotificationsResponse } from "~/client/api-client";
type Props = {
scheduleId: number;
destinations: NotificationDestination[];
+ initialData: GetScheduleNotificationsResponse;
};
type NotificationAssignment = {
@@ -28,13 +30,14 @@ type NotificationAssignment = {
notifyOnFailure: boolean;
};
-export const ScheduleNotificationsConfig = ({ scheduleId, destinations }: Props) => {
+export const ScheduleNotificationsConfig = ({ scheduleId, destinations, initialData }: Props) => {
const [assignments, setAssignments] = useState>(new Map());
const [hasChanges, setHasChanges] = useState(false);
const [isAddingNew, setIsAddingNew] = useState(false);
const { data: currentAssignments } = useQuery({
...getScheduleNotificationsOptions({ path: { scheduleId: scheduleId.toString() } }),
+ initialData,
});
const updateNotifications = useMutation({
diff --git a/app/client/modules/backups/routes/backup-details.tsx b/app/client/modules/backups/routes/backup-details.tsx
index 1e218979..fa928ee0 100644
--- a/app/client/modules/backups/routes/backup-details.tsx
+++ b/app/client/modules/backups/routes/backup-details.tsx
@@ -30,7 +30,13 @@ import { ScheduleSummary } from "../components/schedule-summary";
import type { Route } from "./+types/backup-details";
import { SnapshotFileBrowser } from "../components/snapshot-file-browser";
import { SnapshotTimeline } from "../components/snapshot-timeline";
-import { getBackupSchedule, listNotificationDestinations, listRepositories } from "~/client/api-client";
+import {
+ getBackupSchedule,
+ getScheduleMirrors,
+ getScheduleNotifications,
+ listNotificationDestinations,
+ listRepositories,
+} from "~/client/api-client";
import { ScheduleNotificationsConfig } from "../components/schedule-notifications-config";
import { ScheduleMirrorsConfig } from "../components/schedule-mirrors-config";
import { cn } from "~/client/lib/utils";
@@ -53,13 +59,23 @@ export function meta(_: Route.MetaArgs) {
}
export const clientLoader = async ({ params }: Route.LoaderArgs) => {
- const schedule = await getBackupSchedule({ path: { scheduleId: params.id } });
- const notifs = await listNotificationDestinations();
- const repos = await listRepositories();
+ const [schedule, notifs, repos, scheduleNotifs, mirrors] = await Promise.all([
+ getBackupSchedule({ path: { scheduleId: params.id } }),
+ listNotificationDestinations(),
+ listRepositories(),
+ getScheduleNotifications({ path: { scheduleId: params.id } }),
+ getScheduleMirrors({ path: { scheduleId: params.id } }),
+ ]);
if (!schedule.data) return redirect("/backups");
- return { schedule: schedule.data, notifs: notifs.data, repos: repos.data };
+ return {
+ schedule: schedule.data,
+ notifs: notifs.data,
+ repos: repos.data,
+ scheduleNotifs: scheduleNotifs.data,
+ scheduleMirrors: mirrors.data,
+ };
};
export default function ScheduleDetailsPage({ params, loaderData }: Route.ComponentProps) {
@@ -240,13 +256,18 @@ export default function ScheduleDetailsPage({ params, loaderData }: Route.Compon
schedule={schedule}
/>
-
+
{
- const volumes = await listVolumes();
- const repositories = await listRepositories();
+ const [volumes, repositories] = await Promise.all([listVolumes(), listRepositories()]);
if (volumes.data && repositories.data) return { volumes: volumes.data, repositories: repositories.data };
return { volumes: [], repositories: [] };
diff --git a/app/client/modules/backups/routes/restore-snapshot.tsx b/app/client/modules/backups/routes/restore-snapshot.tsx
index 5b0c2c96..8304c0c0 100644
--- a/app/client/modules/backups/routes/restore-snapshot.tsx
+++ b/app/client/modules/backups/routes/restore-snapshot.tsx
@@ -24,15 +24,20 @@ export function meta({ params }: Route.MetaArgs) {
export const clientLoader = async ({ params }: Route.ClientLoaderArgs) => {
const schedule = await getBackupSchedule({ path: { scheduleId: params.id } });
+
if (!schedule.data) return redirect("/backups");
- const repositoryId = schedule.data.repository.id;
- const snapshot = await getSnapshotDetails({
- path: { id: repositoryId, snapshotId: params.snapshotId },
- });
- if (!snapshot.data) return redirect(`/backups/${params.id}`);
+ const [snapshot, repository] = await Promise.all([
+ getSnapshotDetails({
+ path: {
+ id: schedule.data.repositoryId,
+ snapshotId: params.snapshotId,
+ },
+ }),
+ getRepository({ path: { id: schedule.data.repositoryId } }),
+ ]);
- const repository = await getRepository({ path: { id: repositoryId } });
+ if (!snapshot.data) return redirect(`/backups/${params.id}`);
if (!repository.data) return redirect(`/backups/${params.id}`);
return {
diff --git a/app/client/modules/repositories/routes/restore-snapshot.tsx b/app/client/modules/repositories/routes/restore-snapshot.tsx
index 171deec8..2a8410c6 100644
--- a/app/client/modules/repositories/routes/restore-snapshot.tsx
+++ b/app/client/modules/repositories/routes/restore-snapshot.tsx
@@ -23,12 +23,12 @@ export function meta({ params }: Route.MetaArgs) {
}
export const clientLoader = async ({ params }: Route.ClientLoaderArgs) => {
- const snapshot = await getSnapshotDetails({
- path: { id: params.id, snapshotId: params.snapshotId },
- });
- if (!snapshot.data) return redirect("/repositories");
+ const [snapshot, repository] = await Promise.all([
+ getSnapshotDetails({ path: { id: params.id, snapshotId: params.snapshotId } }),
+ getRepository({ path: { id: params.id } }),
+ ]);
- const repository = await getRepository({ path: { id: params.id } });
+ if (!snapshot.data) return redirect("/repositories");
if (!repository.data) return redirect(`/repositories`);
return { snapshot: snapshot.data, id: params.id, repository: repository.data, snapshotId: params.snapshotId };
diff --git a/app/client/modules/repositories/routes/snapshot-details.tsx b/app/client/modules/repositories/routes/snapshot-details.tsx
index fcf14578..ea10fb6a 100644
--- a/app/client/modules/repositories/routes/snapshot-details.tsx
+++ b/app/client/modules/repositories/routes/snapshot-details.tsx
@@ -26,11 +26,14 @@ export function meta({ params }: Route.MetaArgs) {
}
export const clientLoader = async ({ params }: Route.ClientLoaderArgs) => {
- const snapshot = getSnapshotDetails({
- path: { id: params.id, snapshotId: params.snapshotId },
- });
+ const [snapshot, repository] = await Promise.all([
+ getSnapshotDetails({
+ path: { id: params.id, snapshotId: params.snapshotId },
+ }),
+ getRepository({ path: { id: params.id } }),
+ ]);
- const repository = await getRepository({ path: { id: params.id } });
+ if (!snapshot.data) return redirect(`/repositories/${params.id}`);
if (!repository.data) return redirect("/repositories");
return { snapshot: snapshot, repository: repository.data };
From 9c253cce2cddbe90d7392c1891bb598ed2423bc3 Mon Sep 17 00:00:00 2001
From: Nicolas Meienberger
Date: Tue, 6 Jan 2026 18:35:25 +0100
Subject: [PATCH 11/11] chore: end of line in oxfmtrc
---
.oxfmtrc.json | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/.oxfmtrc.json b/.oxfmtrc.json
index 98859c34..988352d8 100644
--- a/.oxfmtrc.json
+++ b/.oxfmtrc.json
@@ -1,5 +1,6 @@
{
- "$schema": "./node_modules/oxfmt/configuration_schema.json",
- "printWidth": 120,
- "useTabs": true
+ "$schema": "./node_modules/oxfmt/configuration_schema.json",
+ "printWidth": 120,
+ "useTabs": true,
+ "endOfLine": "lf"
}