chore: various fixes

fix(create-schedule): expand folder button submitting the form

style(schedule-form): fix multi scrollbar on long file list
This commit is contained in:
Nicolas Meienberger 2026-01-09 21:25:23 +01:00
parent 5fc17950ba
commit 6e68c75ef1
12 changed files with 65 additions and 15 deletions

5
.env.example Normal file
View file

@ -0,0 +1,5 @@
DATABASE_URL=./data/zerobyte.db
RESTIC_PASS_FILE=./data/restic.pass
RESTIC_CACHE_DIR=./data/restic/cache
ZEROBYTE_REPOSITORIES_DIR=./data/repositories
ZEROBYTE_VOLUMES_DIR=./data/volumes

6
.gitignore vendored
View file

@ -14,3 +14,9 @@ mutagen.yml.lock
notes.md notes.md
smb-password.txt smb-password.txt
cache.db cache.db
data/
.env*
!.env.example
!.env.test

View file

@ -70,6 +70,32 @@ docker compose up -d
Once the container is running, you can access the web interface at `http://<your-server-ip>:4096`. Once the container is running, you can access the web interface at `http://<your-server-ip>:4096`.
## Development (no Docker)
You can run Zerobyte locally during development without Docker:
```bash
bun install
bun run dev
```
For local development, create a `.env.local` file at the repo root and override the Docker paths:
```bash
# Example
DATABASE_URL=./data/zerobyte.db
RESTIC_PASS_FILE=./data/restic.pass
RESTIC_CACHE_DIR=./data/restic/cache
ZEROBYTE_REPOSITORIES_DIR=./data/repositories
ZEROBYTE_VOLUMES_DIR=./data/volumes
```
Notes:
- Remote mount backends (NFS/SMB/WebDAV/SFTP) rely on Linux mount tooling and `CAP_SYS_ADMIN`; on macOS they are expected to be unavailable.
- To actually run backups/repository checks, install `restic` on your machine (e.g. via Homebrew). If `restic` is not installed, the app still starts but backup operations will fail with a clear error.
- rclone support is detected via `RCLONE_CONFIG` or `~/.config/rclone/rclone.conf`.
## Configuration ## Configuration
Zerobyte can be customized using environment variables. Below are the available options: Zerobyte can be customized using environment variables. Below are the available options:

View file

@ -501,6 +501,7 @@ const NodeButton = memo(({ depth, icon, onClick, onMouseEnter, className, childr
return ( return (
<button <button
type="button"
className={cn("flex items-center gap-2 w-full pr-2 text-sm py-1.5 text-left", className)} className={cn("flex items-center gap-2 w-full pr-2 text-sm py-1.5 text-left", className)}
style={{ paddingLeft }} style={{ paddingLeft }}
onClick={onClick} onClick={onClick}

View file

@ -11,7 +11,7 @@ export function GridBackground({ children, className, containerClassName }: Grid
return ( return (
<div <div
className={cn( className={cn(
"relative min-h-full w-full overflow-x-hidden", "relative min-h-full w-full",
"bg-size-[20px_20px] sm:bg-size-[40px_40px]", "bg-size-[20px_20px] sm: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,#e4e4e7_1px,transparent_1px),linear-gradient(to_bottom,#e4e4e7_1px,transparent_1px)]",
"dark:bg-[linear-gradient(to_right,#262626_1px,transparent_1px),linear-gradient(to_bottom,#262626_1px,transparent_1px)]", "dark:bg-[linear-gradient(to_right,#262626_1px,transparent_1px),linear-gradient(to_bottom,#262626_1px,transparent_1px)]",

View file

@ -3,6 +3,7 @@ import { FolderOpen } from "lucide-react";
import { FileTree } from "~/client/components/file-tree"; import { FileTree } from "~/client/components/file-tree";
import { listFilesOptions } from "../api-client/@tanstack/react-query.gen"; import { listFilesOptions } from "../api-client/@tanstack/react-query.gen";
import { useFileBrowser } from "../hooks/use-file-browser"; import { useFileBrowser } from "../hooks/use-file-browser";
import { parseError } from "../lib/errors";
type VolumeFileBrowserProps = { type VolumeFileBrowserProps = {
volumeName: string; volumeName: string;
@ -66,7 +67,7 @@ export const VolumeFileBrowser = ({
if (error) { if (error) {
return ( return (
<div className="flex items-center justify-center h-full min-h-50"> <div className="flex items-center justify-center h-full min-h-50">
<p className="text-destructive">Failed to load files: {(error as Error).message}</p> <p className="text-destructive">Failed to load files: {parseError(error)?.message}</p>
</div> </div>
); );
} }

View file

@ -6,7 +6,9 @@ import { X } from "lucide-react";
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { listRepositoriesOptions } from "~/client/api-client/@tanstack/react-query.gen"; import { listRepositoriesOptions } from "~/client/api-client/@tanstack/react-query.gen";
import { CronInput } from "~/client/components/cron-input";
import { RepositoryIcon } from "~/client/components/repository-icon"; import { RepositoryIcon } from "~/client/components/repository-icon";
import { Button } from "~/client/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card";
import { Checkbox } from "~/client/components/ui/checkbox"; import { Checkbox } from "~/client/components/ui/checkbox";
import { import {
@ -20,13 +22,11 @@ import {
} from "~/client/components/ui/form"; } from "~/client/components/ui/form";
import { Input } from "~/client/components/ui/input"; import { Input } from "~/client/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/client/components/ui/select"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/client/components/ui/select";
import { Button } from "~/client/components/ui/button";
import { Textarea } from "~/client/components/ui/textarea"; import { Textarea } from "~/client/components/ui/textarea";
import { VolumeFileBrowser } from "~/client/components/volume-file-browser"; import { VolumeFileBrowser } from "~/client/components/volume-file-browser";
import { CronInput } from "~/client/components/cron-input";
import { cronToFormValues } from "../lib/cron-utils";
import type { BackupSchedule, Volume } from "~/client/lib/types"; import type { BackupSchedule, Volume } from "~/client/lib/types";
import { deepClean } from "~/utils/object"; import { deepClean } from "~/utils/object";
import { cronToFormValues } from "../lib/cron-utils";
const internalFormSchema = type({ const internalFormSchema = type({
name: "1 <= string <= 32", name: "1 <= string <= 32",
@ -377,7 +377,7 @@ export const CreateScheduleForm = ({ initialValues, formId, onSubmit, volume }:
onSelectionChange={handleSelectionChange} onSelectionChange={handleSelectionChange}
withCheckboxes={true} withCheckboxes={true}
foldersOnly={false} foldersOnly={false}
className="flex-1 border rounded-md bg-card p-2 min-h-75 max-h-100 overflow-auto" className="relative border rounded-md bg-card p-2 h-100 overflow-y-auto"
/> />
{selectedPaths.size > 0 && ( {selectedPaths.size > 0 && (
<div className="mt-4"> <div className="mt-4">
@ -641,8 +641,8 @@ export const CreateScheduleForm = ({ initialValues, formId, onSubmit, volume }:
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
<div className="h-full"> <div className="xl:sticky xl:top-6 xl:self-start">
<Card className="h-full"> <Card>
<CardHeader className="flex flex-row items-center justify-between gap-4"> <CardHeader className="flex flex-row items-center justify-between gap-4">
<div> <div>
<CardTitle>Schedule summary</CardTitle> <CardTitle>Schedule summary</CardTitle>

View file

@ -1,4 +1,6 @@
import * as fs from "node:fs/promises"; import * as fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { logger } from "../utils/logger"; import { logger } from "../utils/logger";
export type SystemCapabilities = { export type SystemCapabilities = {
@ -55,6 +57,11 @@ async function detectRclone(): Promise<boolean> {
} }
async function detectSysAdmin(): Promise<boolean> { async function detectSysAdmin(): Promise<boolean> {
if (process.platform !== "linux") {
logger.warn("sysAdmin capability: disabled. Non-Linux platform detected");
return false;
}
try { try {
const procStatus = await fs.readFile("/proc/self/status", "utf-8"); const procStatus = await fs.readFile("/proc/self/status", "utf-8");

View file

@ -1,7 +1,11 @@
export const OPERATION_TIMEOUT = 5000; export const OPERATION_TIMEOUT = 5000;
export const VOLUME_MOUNT_BASE = "/var/lib/zerobyte/volumes";
export const REPOSITORY_BASE = "/var/lib/zerobyte/repositories"; export const VOLUME_MOUNT_BASE = process.env.ZEROBYTE_VOLUMES_DIR || "/var/lib/zerobyte/volumes";
export const REPOSITORY_BASE = process.env.ZEROBYTE_REPOSITORIES_DIR || "/var/lib/zerobyte/repositories";
export const RESTIC_CACHE_DIR = process.env.RESTIC_CACHE_DIR || "/var/lib/zerobyte/restic/cache";
export const DATABASE_URL = process.env.DATABASE_URL || "/var/lib/zerobyte/data/ironmount.db"; export const DATABASE_URL = process.env.DATABASE_URL || "/var/lib/zerobyte/data/ironmount.db";
export const RESTIC_PASS_FILE = "/var/lib/zerobyte/data/restic.pass"; export const RESTIC_PASS_FILE = process.env.RESTIC_PASS_FILE || "/var/lib/zerobyte/data/restic.pass";
export const DEFAULT_EXCLUDES = [DATABASE_URL, RESTIC_PASS_FILE, REPOSITORY_BASE]; export const DEFAULT_EXCLUDES = [DATABASE_URL, RESTIC_PASS_FILE, REPOSITORY_BASE];

View file

@ -55,7 +55,7 @@ export const runDbMigrations = () => {
} else if (config.__prod__) { } else if (config.__prod__) {
migrationsFolder = path.join("/app", "assets", "migrations"); migrationsFolder = path.join("/app", "assets", "migrations");
} else { } else {
migrationsFolder = path.join("/app", "app", "drizzle"); migrationsFolder = path.join(process.cwd(), "app", "drizzle");
} }
migrate(db, { migrationsFolder }); migrate(db, { migrationsFolder });

View file

@ -4,7 +4,7 @@ import path from "node:path";
import os from "node:os"; import os from "node:os";
import { throttle } from "es-toolkit"; import { throttle } from "es-toolkit";
import { type } from "arktype"; import { type } from "arktype";
import { REPOSITORY_BASE, RESTIC_PASS_FILE, DEFAULT_EXCLUDES } from "../core/constants"; import { REPOSITORY_BASE, RESTIC_PASS_FILE, DEFAULT_EXCLUDES, RESTIC_CACHE_DIR } from "../core/constants";
import { config as appConfig } from "../core/config"; import { config as appConfig } from "../core/config";
import { logger } from "./logger"; import { logger } from "./logger";
import { cryptoUtils } from "./crypto"; import { cryptoUtils } from "./crypto";
@ -107,7 +107,7 @@ export const buildRepoUrl = (config: RepositoryConfig): string => {
export const buildEnv = async (config: RepositoryConfig) => { export const buildEnv = async (config: RepositoryConfig) => {
const env: Record<string, string> = { const env: Record<string, string> = {
RESTIC_CACHE_DIR: "/var/lib/zerobyte/restic/cache", RESTIC_CACHE_DIR,
PATH: process.env.PATH || "/usr/local/bin:/usr/bin:/bin", PATH: process.env.PATH || "/usr/local/bin:/usr/bin:/bin",
}; };

View file

@ -5,7 +5,7 @@
"scripts": { "scripts": {
"lint": "oxlint --type-aware", "lint": "oxlint --type-aware",
"build": "react-router build", "build": "react-router build",
"dev": "bunx --bun vite", "dev": "NODE_ENV=development bunx --bun vite",
"start": "bun ./dist/server/index.js", "start": "bun ./dist/server/index.js",
"cli:dev": "bun run app/server/cli/main.ts", "cli:dev": "bun run app/server/cli/main.ts",
"cli": "ZEROBYTE_CLI=1 bun ./dist/server/index.js", "cli": "ZEROBYTE_CLI=1 bun ./dist/server/index.js",