Compare commits

...

874 commits
test ... main

Author SHA1 Message Date
Nico
7c1f0c52d5
refactor(repo-mutex): use effect.ts (#950)
Some checks failed
Checks / lint (push) Has been cancelled
Checks / typecheck (push) Has been cancelled
Checks / test-server (push) Has been cancelled
Checks / test-client (push) Has been cancelled
Checks / build (push) Has been cancelled
Scorecard analysis / Scorecard analysis (push) Has been cancelled
* refactor(repo-mutex): use effect.ts

* refactor(mutext): run with signal to preserve abort reason
2026-06-06 15:06:26 +02:00
Nico
2318b6bdd0
fix: limit concurrent ls to 2 in flight calls (#948)
* fix: limit concurrent ls to 2 in flight calls

* refactor: get shared lock after semaphore take
2026-06-05 18:07:59 +02:00
Copilot
34fc4f0cbb
docs: update Docker Compose Zerobyte image tags to v0.38 (#945)
* Initial plan

* docs: update Docker Compose Zerobyte image tags to v0.38

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-06-04 21:49:15 +02:00
Nico
68002b0308
fix: disable add passkey in insecure contexts (#943)
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / integration-tests (push) Has been cancelled
Release Workflow / request-docs-version-update (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2026-06-04 21:06:48 +02:00
Nicolas Meienberger
4bba2c2493
refactor: remove repository lock diagnostics logs 2026-06-04 19:58:02 +02:00
Nicolas Meienberger
9d63f7cb2d
Revert "fix(volumes): decrypt values before testing connection (#939)"
Some checks failed
Release Workflow / integration-tests (push) Has been cancelled
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
Release Workflow / request-docs-version-update (push) Has been cancelled
This reverts commit 885ea10f2a.
2026-06-03 19:56:55 +02:00
Nicolas Meienberger
4a4e5c0abe
ci: skip nfs container entirely in github ci 2026-06-03 19:26:45 +02:00
Nico
885ea10f2a
fix(volumes): decrypt values before testing connection (#939)
* fix(volumes): decrypt values before testing connection

* chore: lint issue
2026-06-03 19:05:39 +02:00
Nicolas Meienberger
edab1b231b
fix(restic): validate lock diagnostics object IDs 2026-06-03 18:47:04 +02:00
Nico
333c11986d
feat: enforce protocol version between agent and controller (#938)
* feat: enforce protocol version between agent and controller

* chore: add logging for protocol rejected message
2026-06-03 18:29:36 +02:00
Nicolas Meienberger
be3182793d
fix(agent-backups): validate agent ownership 2026-06-03 17:28:43 +02:00
Nicolas Meienberger
a279129ad8
ci(integration): include only necessary logs in artifacts 2026-06-03 17:21:32 +02:00
Nicolas Meienberger
027c6efb32
ci(docs): allow deploy only on repo default branch 2026-06-03 17:18:10 +02:00
renovate[bot]
755cbe4dae
fix(deps): update bun minor and patch dependencies (#925)
* fix(deps): update bun minor and patch dependencies

* chore: fix linting issues

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Nicolas Meienberger <github@thisprops.com>
2026-06-03 17:15:53 +02:00
Nicolas Meienberger
111a5843ef
refactor: reject all non uv passkeys 2026-06-03 17:03:00 +02:00
renovate[bot]
49e3977199
fix(deps): update dependency fumadocs-mdx to v15 (#870)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-03 16:59:25 +02:00
Nicolas Meienberger
ca325a01c5
chore: re-generate openapi client 2026-06-02 21:04:04 +02:00
renovate[bot]
25f60db703
chore(deps): update docker/build-push-action digest to f9f3042 (#910)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-02 20:56:25 +02:00
renovate[bot]
478a5fcba3
chore(deps): update docker/login-action digest to 650006c (#913)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-02 20:56:03 +02:00
renovate[bot]
bf69fc5f65
chore(deps): update docker/metadata-action digest to 80c7e94 (#915)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-02 20:55:42 +02:00
renovate[bot]
0fd88b2cdf
chore(deps): update docker/setup-buildx-action digest to d7f5e7f (#916)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-02 20:55:18 +02:00
renovate[bot]
8302893233
fix(deps): update dependency commander to v15 (#932)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-02 20:47:47 +02:00
Nicolas Meienberger
1526e3d441
test: automate NFS integration 2026-06-02 20:36:33 +02:00
Nicolas Meienberger
dfd787c8ae
test(integration): skip volume mounting in CI 2026-06-02 20:26:50 +02:00
Nico
a488bbc754
fix: block login for 2fa users with un-verified passkeys (#934)
* fix: block login for 2fa users with un-verified passkeys

* refactor(passkey): show proper login error

* refactor: show passkey generic error on all failures
2026-06-02 19:48:40 +02:00
Nicolas Meienberger
ce23bded90
test: automate SMB integration 2026-06-02 19:45:17 +02:00
Nicolas Meienberger
c793785e30
docs: user and role permission model 2026-06-02 19:21:25 +02:00
Nicolas Meienberger
036382d82d
test: automate webdav integration 2026-06-02 17:30:49 +02:00
Nicolas Meienberger
756ecbddcd
test: automate SFTP integration coverage 2026-06-02 17:25:10 +02:00
Nico
648ccae5fc
test(integration): s3 repository with rustfs & rclone (#933)
* test(integration): s3 repository with rustfs

* ci: run integration tests before release

* chore: fix linting issue

* ci: persist-creds -> false
2026-06-01 21:37:12 +02:00
Nicolas Meienberger
62cdf5dcca
fix(backup-config): throw if include patterns have un-supported characters 2026-06-01 20:14:12 +02:00
Nico
00d1dac515
fix(backup-config): throw if include patterns have un-supported chars (#931) 2026-06-01 20:02:07 +02:00
Nico
d479bfaddc
feat: snapshot restores through rpc (#930)
* feat: snapshot restores through rpc

* fix(restore): do not wait for lock before returning response

* chore: fix liniting issue
2026-05-31 21:48:32 +02:00
Nico
8fedeef4d1
feat: add restore agent RPC foundation (#929)
* feat: add restore agent RPC foundation

* chore: temp event handlers

* refactor: export restore progress from dto file
2026-05-31 19:39:20 +02:00
Nico
2d877cee5a
feat: durable tasks (#927) 2026-05-30 16:54:49 +02:00
Nico
0a2c6bca0c
refactor(restic): auto try to unlock and remove stale locks (#926) 2026-05-30 13:21:22 +02:00
Nico
d4436b0cdc
refactor(restic): all commands return effects (#924)
* refactor(restic): all commands are effects

* fix(restic): preserve effect failure errors

* chore: pr feedbacks
2026-05-30 10:10:54 +02:00
Nicolas Meienberger
ddf7cab503 chore(vite): allow tailscale hosts for dev server 2026-05-30 10:09:01 +02:00
renovate[bot]
74ddf574a8
fix(deps): update bun minor and patch dependencies (#902)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-29 22:24:59 +02:00
Nicolas Meienberger
d502b96509
test: fix webhook mocks 2026-05-29 20:45:16 +02:00
Nicolas Meienberger
dee3bb098a
e2e: fix 2fa leakage in next tests 2026-05-29 20:45:16 +02:00
Nicolas Meienberger
5ee65bf0af
refactor(sftp): mixed options style 2026-05-29 20:45:15 +02:00
Nico
7b5c53bb7d
fix(sftp): allow legacy ssh rsa to add support for older servers (#921)
* refactor(e2e): use more stable assertion

* feat(sftp): add legacy ssh-rsa option
2026-05-27 21:19:23 +02:00
Nicolas Meienberger
17a8838569
deps: bump rclone & shoutrrr to latest versions 2026-05-27 21:16:08 +02:00
Nicolas Meienberger
25b2f71b24
e2e: add missing host-gateway mapping 2026-05-27 21:15:03 +02:00
Nico
008296238b
test(e2e): add test suite to test webhooks (#922) 2026-05-27 21:12:53 +02:00
Nicolas Meienberger
2f78c4a1fb
chore: bump nitro & @better-auth/passkey 2026-05-27 21:07:12 +02:00
Nico
20d202c20e
fix(core): enforce webhook timeout (#920) 2026-05-27 20:35:01 +02:00
Nicolas Meienberger
47f8f2f03a
docs: update local dev server instructions 2026-05-27 17:40:43 +02:00
Nicolas Meienberger
4ea9f34154
chore: portless setup 2026-05-27 17:14:34 +02:00
Nico
e4898b97ea
fix(2fa): add missing 2fa column (#917) 2026-05-22 20:09:30 +02:00
Nico
98338e80c3
Add passkey authentication support (#845)
* feat(auth): add passkey authentication support

* fix: implement AI review feedback

* fix: use non-unique index for passkey_credentialID_idx in migration

* refactor(passkeys): use TanStack mutations for passkey CRUD operations

* chore: restore lockfile from main and add @better-auth/passkey

* chore: fix conflicts

* refactor(passkey-login): simplify passkey autofill event

* refactor(settings-passkeys): ux improvements

---------

Co-authored-by: Nicolas Meienberger <github@thisprops.com>
2026-05-21 21:18:46 +02:00
Nico
273408cdb8
fix(shutdown): keep the current volume status after shutdown cleanup (#906)
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
Release Workflow / request-docs-version-update (push) Has been cancelled
2026-05-20 15:04:48 +02:00
Copilot
0589fd63b1
Update Docker Compose docs examples to Zerobyte v0.37 image tag (#905)
* Initial plan

* docs: update docker compose image tags to v0.37

Agent-Logs-Url: https://github.com/nicotsx/zerobyte/sessions/067f22a5-8223-4fd3-8e9c-c50f6ab2cc6e

Co-authored-by: nicotsx <47644445+nicotsx@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: nicotsx <47644445+nicotsx@users.noreply.github.com>
2026-05-20 14:38:51 +02:00
Nicolas Meienberger
6bcb0bef71
chore: bump rclone & shoutrrr versions
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
Release Workflow / request-docs-version-update (push) Has been cancelled
2026-05-20 13:41:34 +02:00
renovate[bot]
705b8eb748
fix(deps): update bun minor and patch dependencies (#892)
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
Release Workflow / request-docs-version-update (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-19 22:00:51 +02:00
renovate[bot]
05073e667e
chore(deps): update cloudflare/wrangler-action action to v4 (#884)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-19 21:56:11 +02:00
renovate[bot]
970a7fa42f
fix(deps): update dependency content-disposition to v2 (#880)
* fix(deps): update dependency content-disposition to v2

* refactor(content-disposition): use new named export

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Nicolas Meienberger <github@thisprops.com>
2026-05-19 21:55:43 +02:00
renovate[bot]
575e01cab9
chore(deps): update dependency bun to v1.3.14 (#885)
* chore(deps): update dependency bun to v1.3.14

* chore: update bun base docker image

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Nicolas Meienberger <github@thisprops.com>
2026-05-19 21:28:46 +02:00
renovate[bot]
eb36621b30
chore(deps): update actions/upload-artifact digest to 043fb46 (#777)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-19 20:51:48 +02:00
renovate[bot]
da1d4d30b2
chore(deps): update actions/cache digest to 27d5ce7 (#786)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-19 20:51:23 +02:00
renovate[bot]
68a7b0beb1
chore(deps): update github/codeql-action digest to 9e0d7b8 (#793)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-19 20:51:04 +02:00
renovate[bot]
4d5a5d399b
chore(deps): update voidzero-dev/setup-vp digest to ca1c466 (#866)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-19 20:50:45 +02:00
Nico
f82494abab
test: add repository import e2e coverage (#901) 2026-05-19 20:46:08 +02:00
Nico
4dcafa0708
feat(auth): allow skipping forced recovery key download (#900)
* feat(auth): allow skipping forced recovery key download

* refactor: move from session storage to cookie
2026-05-19 20:36:45 +02:00
Nico
c071596151
fix(system): block recovery key download without credential password (#899) 2026-05-19 20:28:09 +02:00
Nico
4b66ad73a7
fix(auth): verify reauth passwords against credential account (#898) 2026-05-19 20:20:55 +02:00
Nico
4a06891db9
ci: remove stale file upload (#897) 2026-05-19 20:14:16 +02:00
Nico
3b9f2086ee
fix(backups): preserve retry settings when toggling schedules (#896) 2026-05-19 20:08:05 +02:00
Nico
94f6d0529f
refactor(mutext): persist repository locks in database (#895)
* refactor(mutext): persist repository locks in database

* fix: clean up promoted repository lock on queued abort

* fix: throttle repository lock cleanup during polling
2026-05-19 18:59:40 +02:00
Nicolas Meienberger
b292f94186
refactor(crypto): reject encrypted values from another secret in sealSecret 2026-05-18 22:00:21 +02:00
Nicolas Meienberger
66ebc249ca
fix: force recovery key redownload after possible truncated download 2026-05-18 21:35:53 +02:00
Nicolas Meienberger
0f5a6823ec
refactor(crypto): reject encrypted values from another secret 2026-05-18 21:14:20 +02:00
Nicolas Meienberger
419204d587
fix(download-password): revoke url after 60 secs 2026-05-18 21:13:07 +02:00
Nico
11dacd7c71
refactor(agent): store SFTP volume keys in tmp (#888)
* refactor(agent): store SFTP volume keys in tmp

* refactor: store temp keys in /run/zerobyte subfolders

* fix(restic): clean temp secrets dir on env setup failure

* fix: one secrets temp dir per env building
2026-05-17 18:19:09 +02:00
Nico
19a0781667
test: backend integration (#889)
* test: backend integration

* docs: mounted shares acls

* feat: smb expose real ACLs when available

* fix: re-init repo on setup

* chore: add missing @hono/standard-validator package

* chore: add happy-dom dev dep
2026-05-17 15:18:56 +02:00
Nico
3142cb026a
chore: move access logs to debug (#891) 2026-05-17 14:26:40 +02:00
Nico
2a1351382f
fix(agent-manager): disable ws server when not needed (#890) 2026-05-17 14:04:49 +02:00
renovate[bot]
0dccd00539
fix(deps): update bun minor and patch dependencies (#867)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-17 13:54:02 +02:00
Nicolas Meienberger
ecbf407058
deps: minimum release age -> 3 days 2026-05-16 11:59:34 +02:00
John Veness
18a91d9b78
README: Fix some punctuation, styling, grammar (#879)
* README: Fix some punctuation, styling, grammar

* Apply suggestion from @JohnVeness

* Update README.md
2026-05-13 17:08:07 +02:00
Nico
a58fe82d48
refactor: move backup path resolution into agent contract (#872) 2026-05-10 21:46:03 +02:00
Nico
aa7da321ba
refactor: dedpulicate volume schemas across packages (#864) 2026-05-09 15:36:25 +02:00
Nico
2ada5acd5a
refactor(agent): harden local agent volume lifecycle (#863)
* refactor(agent): harden local agent volume lifecycle

* chore(test): remove un-used variable

* refactor(agent): create dedicated jobs for recurring tasks

* chore: pr feedbacks

* test: add missing fake agent controller
2026-05-09 12:13:04 +02:00
Nico
2062beac68
refactor(server): route volume operations through agents (#862)
* refactor(server): route volume operations through agents

* chore: pr feedbacks
2026-05-07 20:51:25 +02:00
Nico
df4b668560
feat(agent): add volume operation RPC (#861) 2026-05-07 18:11:57 +02:00
renovate[bot]
5e4742488f
fix(deps): update bun minor and patch dependencies (#865)
* fix(deps): update bun minor and patch dependencies

* fix: ci

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Nicolas Meienberger <github@thisprops.com>
2026-05-07 17:11:53 +02:00
renovate[bot]
cda008a53e
chore(deps): update voidzero-dev/setup-vp digest to 4f5aa3e (#818)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-07 17:08:04 +02:00
Nicolas Meienberger
e91df3eab0
chore(renovate): range strategy bump 2026-05-07 09:32:42 +02:00
Nicolas Meienberger
3c7eb65c7c
chore(package-core): remove lock 2026-05-07 09:17:46 +02:00
Nicolas Meienberger
b13b6d606a
chore(renovate): disable minimumRelease age for gh actions 2026-05-07 08:48:01 +02:00
Nicolas Meienberger
66cd9e185b
chore(renovate): rangeStrategy bump 2026-05-07 08:17:45 +02:00
Nicolas Meienberger
1ca1598c58
fix: change dev panel shortcut to D+E+V 2026-05-07 08:17:44 +02:00
renovate[bot]
89ae52d036
chore(deps): update docker/build-push-action digest to bcafcac (#772)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-07 08:10:59 +02:00
Nico
700fa1cd4a
feat: route backups through volume agents (#860) 2026-05-06 22:13:51 +02:00
Nico
e65a135676
feat(agents): create agent registry and service (#849)
* feat(agents): create agent registry and service

* fix: mark agent offline only if the session was removed properly

* refactor: centralize agent backup lifecycle state

* refactor: simplify session management

* refactor: move effect / async boundary in one place

* chore: regen migration

* refactor: improve error handling

* chore: pr feedback
2026-05-05 19:34:10 +02:00
Nicolas Meienberger
e981211a2d
fix(notifications): validate webhook headers and show delivery health 2026-05-05 10:24:20 +02:00
Nicolas Meienberger
cd69eea27f
fix: avoid unnecessary webhook allowlist checks on notification edits 2026-05-05 10:24:20 +02:00
Nicolas Meienberger
b1ae85e2c1
fix(notifications): preserve existing destinations with target allowlist 2026-05-05 10:24:20 +02:00
Copilot
497a0e8bee
docs: update Docker Compose image tags to v0.36 (#857)
* Initial plan

* docs: update Docker image tags to v0.36

Closes nicotsx/zerobyte#856

Agent-Logs-Url: https://github.com/nicotsx/zerobyte/sessions/685fd718-282c-4843-b5bb-082bf8ed0571

Co-authored-by: nicotsx <47644445+nicotsx@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: nicotsx <47644445+nicotsx@users.noreply.github.com>
2026-05-04 22:02:35 +02:00
Nico
147f266929
fix(notifications): enforce allowlist for custom Shoutrrr targets (#853)
Some checks failed
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
Release Workflow / request-docs-version-update (push) Has been cancelled
* fix(notifications): enforce allowlist for custom Shoutrrr targets

* fix(notifications): enforce allowlist for email notification targets
2026-05-04 17:47:44 +02:00
Nico
0351d5e0b9
refactor(auth): translate better-auth errors allowedHosts (#854)
Closes #852
2026-05-04 17:47:25 +02:00
Nicolas Meienberger
f0ac9fe35c
chore: update new issue link to show templates 2026-05-04 17:09:46 +02:00
Nicolas Meienberger
17d6ec85c3
chore: bump rclone to 1.74.0 2026-05-04 17:05:19 +02:00
Nicolas Meienberger
e7b9b118a8
docs: backup webhooks 2026-05-04 08:12:24 +02:00
Nicolas Meienberger
8772d4796e
feat(backups): configure backup webhook timeout
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
Release Workflow / request-docs-version-update (push) Has been cancelled
2026-05-04 07:52:21 +02:00
Nicolas Meienberger
38f5a669ae
fix(core): preserve significant path whitespace 2026-05-04 07:19:57 +02:00
Nicolas Meienberger
7be7c9edae
test(e2e): fix waiting on transitive state
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / request-docs-version-update (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2026-05-03 20:51:43 +02:00
Nico
f83b765d04
fix(notifications): persist delivery health status (#850)
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
Release Workflow / request-docs-version-update (push) Has been cancelled
* fix(notifications): persist delivery health status

* fix: pr feedback double update
2026-05-02 11:51:16 +02:00
Antoine Jeanselme
3d5a0a9b75
feat: add repositories & volumes column sorting (#808)
Some checks failed
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
Release Workflow / request-docs-version-update (push) Has been cancelled
* Add repository column sorting

* Make status title correctly centered o nsmaller screens

* Add volumes column sorting

* refactor: use tanstack table for filtering and sorting

* feat: make notifications sortable

* chore: pr feedbacks

---------

Co-authored-by: Antoine Jeanselme <67123340+ajeanselme@users.noreply.github.com>
Co-authored-by: Nicolas Meienberger <github@thisprops.com>
2026-05-02 10:21:42 +02:00
Nicolas Meienberger
d19d827496
fix(backups): default ignore-inode for fuse volumes 2026-05-02 08:54:14 +02:00
Nicolas Meienberger
35dd49d7ac
docs: add sections about new WEBHOOK_ALLOWED_ORIGINS env var 2026-05-01 18:26:13 +02:00
Nicolas Meienberger
94d7fdc9a8
fix(notifications): validate notification webhook origins 2026-05-01 18:22:53 +02:00
Nicolas Meienberger
8fd54383a8
docs: add reference CLI usage 2026-05-01 18:07:52 +02:00
Nico
220e2d2801
feat: make webhook calls trusted only (#848)
* feat: make webhook calls trusted only

* fix: pr feedbacks
2026-04-30 22:14:22 +02:00
Nico
1cf96c5292
refactor(agents): split local agent bootstrap from controller startup (#847) 2026-04-30 21:39:55 +02:00
Nico
11e9fbcc44
feat: pre/post backup webhooks (#835)
* feat: pre/post backup webhooks

* fix(hooks): run post when cancelled

* refactor(webhooks): headers as array

* refactor: pr feedback

* refactor: simplify hooks ceremonies

* chore: pr feedbacks

* chore: re-gen migration
2026-04-29 23:48:58 +02:00
renovate[bot]
6728271a98
fix(deps): update bun minor and patch dependencies (#842)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-29 21:12:26 +02:00
Copilot
82e027d872
docs: bump Docker image tags to v0.35 (#839)
* Initial plan

* docs: update Docker image tags to v0.35

Agent-Logs-Url: https://github.com/nicotsx/zerobyte/sessions/9862acc4-1f34-4972-b08a-f334ba1de312

Co-authored-by: nicotsx <47644445+nicotsx@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: nicotsx <47644445+nicotsx@users.noreply.github.com>
2026-04-25 14:58:18 +02:00
Nicolas Meienberger
f3eb072182
ci: run update docs workflow only if minor is released 2026-04-25 14:53:54 +02:00
Nicolas Meienberger
eafcf2dd1f
ci: include issue to close to copilot instructions 2026-04-25 14:51:58 +02:00
Nicolas Meienberger
c216fb1430
ci: auto update docs and readme version 2026-04-25 14:44:48 +02:00
Nicolas Meienberger
d567dd9867
docs(readme): update version 2026-04-25 14:38:07 +02:00
Nico
2000ebd254
chore(Dockerfile): pin base image digest (#834) 2026-04-25 09:44:23 +02:00
Nico
a5d47eac5a
fix(system): ensure version check is not cached by browser (#833)
#798
2026-04-25 09:41:29 +02:00
Nico
e506047415
test: add property-based tests with fast-check (#832) 2026-04-25 09:35:28 +02:00
Nicolas Meienberger
7b3b7583c0
chore: improve sanitize function to catch escaped characters 2026-04-25 09:30:16 +02:00
Nico
f078e006c1
fix(notifications): encode SMTP credentials in Shoutrrr URLs (#831)
Closes #829
2026-04-25 09:24:05 +02:00
Nicolas Meienberger
3adc722cca
docs: update SECURITY.md with trust model baseline 2026-04-25 09:03:42 +02:00
Nicolas Meienberger
c64862f604
refactor: extract shared test fixtures and secret helpers 2026-04-22 22:33:18 +02:00
Nico
26203cca59
chore: remove unused deps (#828) 2026-04-22 22:17:51 +02:00
Nico
6ae5a33acd
chore: remove dead files (#827) 2026-04-22 22:13:24 +02:00
Nico
4631655c3e
docs: add security policy (#826) 2026-04-22 22:10:24 +02:00
Nico
10eb9a84fb
ci: restrict workflow GITHUB_TOKEN permissions (#825) 2026-04-22 22:07:24 +02:00
Nico
e2c9ef0518
chore: fixed unused types (#824) 2026-04-22 22:04:46 +02:00
renovate[bot]
04973849a0
chore(deps): update github/codeql-action action to v4 (#822)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-22 21:15:03 +02:00
Nico
a0c34ee48d
chore: cleanup un-used exports (#823) 2026-04-22 21:14:37 +02:00
Nico
3f92dace6c
ci: add scorecard analysis (#820) 2026-04-21 21:39:18 +02:00
Nicolas Meienberger
16085a5fed
chore: bump Bun to 1.3.13 2026-04-21 19:10:48 +02:00
renovate[bot]
d2a0f75e2e
chore(deps): update dependency bun to v1.3.13 (#816)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-21 19:09:20 +02:00
Nicolas Meienberger
c22af17fa6
fix(volumes): avoid ENOENT during test connection cleanup
Some checks failed
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2026-04-20 23:06:26 +02:00
Nicolas Meienberger
f967450eea
fix(volumes): unmount existing mounts before remounting
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2026-04-20 22:34:58 +02:00
Nicolas Meienberger
694f1c212f
fix(volumes): isolate test-connection mounts in temp directories 2026-04-20 22:28:42 +02:00
Nicolas Meienberger
8ece1ef187
test: fix e2e assertions in sync snapshots
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2026-04-20 22:00:38 +02:00
Nicolas Meienberger
c95cc305f8
chore: bump rclone to 1.73.5 2026-04-20 21:26:19 +02:00
Nico
63b08a4b71
feat(backups): ensure volume readiness before backup (#815)
* feat(backups): ensure volume readiness before backup

Backup preflight checks were relying on a db cached status. Now a real
volume healthcheck is performed before backing up. Closes #811 #695

* chore: fix ci
2026-04-20 21:18:29 +02:00
Nicolas Meienberger
96609fc328
docs: update configuration to mention APP_SECRET_FILE 2026-04-20 19:47:14 +02:00
Nicolas Meienberger
927097a902
chore(packages): change stability days to 1 day 2026-04-20 19:46:54 +02:00
Nicolas Meienberger
cbc874df0d
docs: improve page metadata 2026-04-18 11:30:33 +02:00
Nicolas Meienberger
e19776a2b8
docs: add sitemap.xml 2026-04-18 11:09:56 +02:00
Nicolas Meienberger
c5e35cf8b1
docs: refactor notifications to use tabs 2026-04-18 10:55:49 +02:00
Nicolas Meienberger
ff9decb54b
docs: restoring guide 2026-04-18 10:19:38 +02:00
Nico
0224afae4a
docs: improve provisioning section with more details (#810)
Closes #805
2026-04-18 09:20:20 +02:00
renovate[bot]
72c2f89cc8
chore(deps): update dependency typescript to v6 (#800)
* chore(deps): update dependency typescript to v6

* chore(docs): add tsc script

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Nicolas Meienberger <github@thisprops.com>
2026-04-16 21:53:50 +02:00
renovate[bot]
6f0617492b
fix(deps): update bun minor and patch dependencies (#795)
* fix(deps): update bun minor and patch dependencies

* chore: update api-client

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Nicolas Meienberger <github@thisprops.com>
2026-04-16 21:46:05 +02:00
Nicolas Meienberger
dd912b8246
ci: fix docker build 2026-04-16 21:38:42 +02:00
renovate[bot]
d56ee35b06
chore(deps): update dependency @types/node to v25 (#796)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-16 21:38:02 +02:00
renovate[bot]
2619da9c5f
chore(deps): pin dependencies (#794)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-16 21:29:59 +02:00
Eric Hess
d2f65716fe
feat(mirrors): add selective snapshot sync to mirror repositories (#755)
* feat(mirrors): add selective snapshot sync to mirror repositories

Allow users to sync missing snapshots from the source repository to a
mirror. A new sync button per mirror opens a dialog showing which
snapshots are missing, with checkboxes to select which ones to copy.

- Add GET /:shortId/mirrors/:mirrorShortId/status endpoint to compare
  snapshots between source and mirror repositories
- Add POST /:shortId/mirrors/:mirrorShortId/sync endpoint accepting
  selected snapshotIds in the request body
- Extend restic copy command to accept multiple snapshotIds
- Add sync preview dialog with snapshot selection to the frontend

* refactor: stylistic changes

---------

Co-authored-by: Nicolas Meienberger <github@thisprops.com>
2026-04-16 21:28:48 +02:00
renovate[bot]
497fa474a7
fix(deps): update dependency lucide-react to v1 (#804)
* fix(deps): update dependency lucide-react to v1

* fix(deps): replace lucide brand icons with phosphor ones

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Nicolas Meienberger <github@thisprops.com>
2026-04-16 21:27:19 +02:00
renovate[bot]
28b89036f1
chore(deps): update dependency jsdom to v29 (#799)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-16 21:15:35 +02:00
renovate[bot]
308d9916ba
chore(deps): update dependency vitest to v4 (#803)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-16 21:09:53 +02:00
Nicolas Meienberger
95006a7471
chore(core package): re-organize files 2026-04-16 19:07:11 +02:00
Nico
5b728b8144
chore: move docs to monorepo apps (#801)
* chore: move docs to monorepo apps

* chore(docs): bump vite
2026-04-16 19:03:00 +02:00
renovate[bot]
1e98013e45
chore(deps): update dependency @vitejs/plugin-react to v6 (#797)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-16 18:37:44 +02:00
renovate[bot]
e059c35fbb
chore(deps): update softprops/action-gh-release action to v3 (#790)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-16 18:35:08 +02:00
Nico
d10a3d2d65
docs: init project (#792)
* docs: init project

docs: design

content

header sticky

docs: content

docs: update starting guide

docs: corrections

docs: oidc, sso & more

feat: landing page

style: card design

style: colors

style: zerobyte logo

style: corner content

style: docs cards

ci(docs): auto deploy to cloudflare

docs: 3-2-1 strategy

* fix: anchor links

* style: refactor landing hero

* feat: og

* chore: fix ci

* ci: build docs before publishing
2026-04-15 23:13:10 +02:00
Nico
33601dde24
feat(agent): add standalone agent runtime (#776)
* feat(agent): add standalone agent runtime

* fix(agent): add Bun and DOM types to agent tsconfig

* refactor: wrap backup error in a tagged effect error

* feat(controller): add agent manager and session handling

* feat(backups): execute backups through the agent

* fix(agent): harden disconnect and send-failure handling

* fix: rebase conflicts

* test: simplify mocks

* refactor: split agent runtime state

* fix(backup): keep old path when agent is disabled

* fix: pr feedbacks
2026-04-13 23:29:10 +02:00
renovate[bot]
7fb5e6d65d
chore(deps): update dependency @effect/language-service to ^0.85.0 (#781)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-13 20:56:18 +02:00
Nicolas Meienberger
b25ccdc6b2
chore: bump shoutrr and rclone versions 2026-04-12 13:41:05 +02:00
Nicolas Meienberger
5497cadb56
chore: bump Bun to 1.3.12 2026-04-12 13:35:52 +02:00
Nicolas Meienberger
2ecd4b8eb6
chore: bump drizzle-orm 2026-04-12 13:34:25 +02:00
Nicolas Meienberger
76ac78c40c
chore: fix ts issues in test files
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2026-04-12 11:39:06 +02:00
renovate[bot]
97b4ce42c4
chore(deps): update bun minor and patch dependencies (#764)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-12 11:30:23 +02:00
Nicolas Meienberger
832425025f
refactor(backup): move retry delay minutes conversion at the contract level 2026-04-12 11:28:39 +02:00
Nicolas Meienberger
7ea9899385
Merge branch 'Der-Penz-retry-backup' 2026-04-12 10:50:02 +02:00
Nicolas Meienberger
ed046f284d
fix(backup): reset failure count on cancel 2026-04-12 10:39:14 +02:00
Nicolas Meienberger
cb22e34215
refactor(backup): default to 2 retries & 15 minutes delay 2026-04-12 10:16:26 +02:00
DerPenz
8765c5e705 feature(backup): do not trigger retry on manual backup 2026-04-12 09:49:35 +02:00
DerPenz
11bb95db92 fix(backup): retry wont be scheduled if it is after the next scheduled backup 2026-04-12 09:49:35 +02:00
DerPenz
5d8c2925d8 fix(backup): resetting failureRetryCount if backup is not retried 2026-04-12 09:49:35 +02:00
DerPenz
61396625f1 fix(backup): added limits in zod schema 2026-04-12 09:49:35 +02:00
DerPenz
b2728cb09a fix(backup): removed useless null checks 2026-04-12 09:49:35 +02:00
DerPenz
0476dcebb8 fix(backup): updating new settings correctly in the db 2026-04-12 09:49:35 +02:00
DerPenz
b1cd1a4220 fix(backup): added placeholder in settings 2026-04-12 09:49:35 +02:00
DerPenz
79b7733e1e fix(backup): updated types gen 2026-04-12 09:49:35 +02:00
DerPenz
7e9b1fafa3 feat(backup): added retry policy into job settings 2026-04-12 09:49:35 +02:00
DerPenz
feb69ef7a7 feat(backup): retry backup on failure 2026-04-12 09:49:35 +02:00
Nico
4520335ebc
fix(rclone): pass explicitly the RCLONE_CONF env var (#779) 2026-04-12 09:25:57 +02:00
Nicolas Meienberger
c25eacad05
ci: disable vite hooks in docker build 2026-04-11 19:14:18 +02:00
Nico
73ae5c41b5
style: fix misalignment in mirrors ui (#778) 2026-04-11 08:57:46 +02:00
renovate[bot]
1f4f94b3a5
chore(deps): pin voidzero-dev/setup-vp action to 8ecb391 (#775)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-10 22:36:47 +02:00
Nicolas Meienberger
d213d3546a
ci: install vp globally 2026-04-10 22:33:26 +02:00
Nico
28ba8657f9
feat(runtime): start and ship the local agent (#767)
* feat(runtime): start and ship the local agent

* refactor: gate local agent behind feature flag

* chore: skip agent manager if flag is false

* fix: hot reload agents

* test: fix config tests
2026-04-10 00:00:30 +02:00
Nico
863fbfc5cc
fix(snapshots): use streamed response to avoid 1 MiB buffer limit on large repos (#771) 2026-04-09 23:47:36 +02:00
Nico
70c7de1efc
feat(config): add support for APP_SECRET as a file (#769)
* feat(config): add support for APP_SECRET as a file

* fix(tsc): ensure appSecret is set in type system

* fix: pr feedback
2026-04-09 23:13:07 +02:00
Nico
595a29056d
fix(slack): channel is not supported in combination with webhooks (#770)
#766
2026-04-09 22:24:24 +02:00
Nico
4872029b87
fix(smb-backend): add explicit utf8 charset encoding (#768) 2026-04-09 21:39:19 +02:00
Nicolas Meienberger
979e494e75
docs: update readme 2026-04-09 21:18:25 +02:00
Nico
5bcc3acc56
feat(controller): add agent manager and session handling (#763) 2026-04-09 20:00:54 +02:00
Nico
8a4a06f888
chore: migrate toolchain to vite-plus (#762)
* chore: migrate toolchain to vite-plus

* chore: run lint with bunx

* chore: add vp config postinstall
2026-04-09 17:57:19 +02:00
Nicolas Meienberger
f9027033e5 ci: add environment to release job 2026-04-08 20:59:55 +02:00
renovate[bot]
8c14a2742c
chore(deps): update bun minor and patch dependencies (#751)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-08 20:47:56 +02:00
Nico
c371676ad0
feat(agent): add standalone agent runtime (#761)
* feat(agent): add standalone agent runtime

* fix(backups): bridge local executor to Effect restic API

* fix(agent): add Bun and DOM types to agent tsconfig

* refactor: wrap backup error in a tagged effect error

* fix: pr feedbacks
2026-04-08 20:47:15 +02:00
Nico
3169627b79
feat(contracts): add shared agent/controller protocol (#759)
* feat(contracts): add shared agent/controller protocol

* chore: pr feedback
2026-04-08 08:17:15 +02:00
Nicolas Meienberger
049becb900 refactor(mutex): improve multi locks operations to wait for all
Wait for all locks to be available instead of locking one side and
waiting
2026-04-07 18:13:02 +02:00
Nico
ad8a83e60e
e2e: refactor oidc server from dex to tinyauth (#754) 2026-04-06 17:48:37 +02:00
Nicolas Meienberger
f11e3b7f82 fix(schedule): disable if saving with manual-only mode
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2026-04-04 23:23:26 +02:00
Nicolas Meienberger
74d20d5be3 test(e2e): fix locator for delete volume button now in dropdown
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2026-04-04 20:47:19 +02:00
renovate[bot]
7add3dd517
chore(deps): update dependency typescript to v6 (#732)
* chore(deps): update dependency typescript to v6

* chore: fix bun types in core package

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Nicolas Meienberger <github@thisprops.com>
2026-04-04 17:34:25 +02:00
Nico
9e7f1bf138
fix: windows path style in snapshot (#742) 2026-04-04 17:21:13 +02:00
Nicolas Meienberger
43d9cb837f refactor(datetime): always use 2 digits for day and month formatting
Fixes #744
2026-04-04 17:19:53 +02:00
renovate[bot]
1cb1a66ef1
fix(deps): update bun minor and patch dependencies (#733)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-04 17:16:03 +02:00
Nico
a4e54ac527
fix: serialize mirror copy operations per destination repository (#747)
* fix: serialize mirror copy operations per destination repository

* refactor(copy-mirror): implement acquireMany to avoid deadlock cycles
2026-04-04 17:12:45 +02:00
Nico
bae43c0d90
fix: sanitize Accept-Language locale parsing (#746)
* fix: sanitize Accept-Language locale parsing

* refactor(accept-language): prefer the highest quality language
2026-04-04 13:18:04 +02:00
renovate[bot]
afc77f55b8
chore(deps): pin dependencies (#729)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-02 23:44:53 +02:00
Nico
95aadf6e73
refactor: dedicated edit page for notifications (#741) 2026-04-02 22:59:23 +02:00
Nico
731ebad1ee
style: redesign volume details page to match repository (#740) 2026-04-02 22:55:23 +02:00
Nico
475bfb59ae
style: redesign respository details page (#739) 2026-04-02 22:51:57 +02:00
Nico
e77723164b
refactor: improve buttons and space with dropdowns (#738) 2026-04-02 21:44:32 +02:00
Nico
4bf1463406
refactor: dedicated edit page for volumes (#737) 2026-04-02 21:40:24 +02:00
Nico
d6e80b71d7
refactor: dedicated edit page for backups (#736) 2026-04-02 21:36:31 +02:00
renovate[bot]
a6d402b915
fix(deps): update bun minor and patch dependencies (#730)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-01 23:11:22 +02:00
Nicolas Meienberger
0baf4677e7 chore(bun): switch minimum release age to 3 days
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2026-04-01 20:53:21 +02:00
Nicolas Meienberger
d3da24c916 chore(renovate): switch minimum release age to 3 days 2026-04-01 20:51:09 +02:00
Nicolas Meienberger
f7324db5fc chore(renovate): fix invalid config 2026-04-01 20:14:32 +02:00
Nico
4305057185
test: move test runner from Bun to Vitest (#727)
* chore: migrate to vitest

* test: speed up some suites by sharing sessions and mocking expensive non-tested actions

* test: refactor some tests to verify behavior instead of implementation details

* chore: fix linting issues
2026-04-01 20:05:54 +02:00
Nicolas Meienberger
e265f7d478 test: fix flaky test depending on mutex timing
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2026-04-01 09:27:13 +02:00
Nicolas Meienberger
add0f2788f fix: restoring snapshots that have unrelated root paths
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2026-03-31 23:05:19 +02:00
Nicolas Meienberger
63e12868b1 fix: react hydration issues by using the same locale during ssr and hydration
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2026-03-31 22:39:16 +02:00
Nicolas Meienberger
ca38f7ca69 fix(select): render a div durng ssr to avoid hydration issues 2026-03-31 22:05:46 +02:00
Nicolas Meienberger
915b96c218 refactor: split out root-loader data getter into a dedicated function 2026-03-31 21:19:16 +02:00
Nicolas Meienberger
3acf565ccd fix: force user to provide known hosts values in sftp volume / repository creation
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2026-03-31 20:27:48 +02:00
Kevin Stallone
aa7eaa0929
feat(volumes): add webdav, sftp, and rclone filters (#720)
* feat(volumes): add sftp, webdav, and rclone filters

* chore(volumes): align backend filter order with create-volume form
2026-03-31 20:27:43 +02:00
Nicolas Meienberger
c9451d90dd chore: bump rclone and shoutrrr 2026-03-31 20:03:55 +02:00
Nico
d600102d46
chore: move from dependabot to renovate (#721)
* chore: move from dependabot to renovate

* ci: pin oxlint version
2026-03-31 19:35:18 +02:00
Nicolas Meienberger
e55ffccd21 feat: infer default time format from navigator.language 2026-03-30 18:44:26 +02:00
Nico
f89b8c3b52
feat: custom date time format (#718) 2026-03-29 17:24:54 +02:00
Nico
2a219ac042
refactor(backups): consolidate execution into service helpers (#717) 2026-03-29 12:57:47 +02:00
Nico
611640b32b
fix: split display path and query base path (#714)
* fix: split display path and query base path

#709

* test(frontend): shared render utils

* fix(file-tree): add missing path segments

* chore: bump Bun version

* fix: select kind for synthetic folders
2026-03-29 12:30:35 +02:00
Nico
866a3c63e8
feat: manual backup option (#713)
Closes #710
2026-03-26 19:35:18 +01:00
Nicolas Meienberger
6354705626 test(e2e): isolated data per worker 2026-03-24 09:44:36 +01:00
Nicolas Meienberger
b4668e23c7 test(e2e): attach browser logs and full errors in report 2026-03-24 07:52:54 +01:00
Nicolas Meienberger
7ef211ddd5 docs: update readme version 2026-03-24 07:52:54 +01:00
Nicolas Meienberger
4c928cbc33 refactor(client): configure time formatting with timezone from server 2026-03-24 07:52:54 +01:00
dependabot[bot]
5a83d6ae86
chore(deps): bump lucide-react from 0.577.0 to 1.0.1 (#706) 2026-03-24 07:38:48 +01:00
Nico
8acca9ef4a
fix: cli not running (#703) 2026-03-23 19:47:44 +01:00
Nicolas Meienberger
c93b076bf6 test: simplify file-tree selection & login mocking 2026-03-22 12:06:48 +01:00
Nico
4fec2777ce
test(e2e): fail in unexpected console.error (#696)
* test(e2e): fail in unexpected console.error

* fix(datetime): graceful fallback during SSR when navigator is undefined
2026-03-22 11:31:48 +01:00
Nicolas Meienberger
dd1aca7e30 test(backup): ensure warning details are returned 2026-03-22 11:06:38 +01:00
Nicolas Meienberger
27b1789657 chore(dockerfile): fail early when curl does not get the correct format 2026-03-22 11:06:10 +01:00
Nico
05dd440dea
fix(restic): separate error summary from diagnostic details (#694)
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2026-03-21 20:50:23 +01:00
dependabot[bot]
962ab01ea4
chore(deps): bump the minor-patch group with 6 updates (#684)
Bumps the minor-patch group with 6 updates:

| Package | From | To |
| --- | --- | --- |
| [@tanstack/react-query](https://github.com/TanStack/query/tree/HEAD/packages/react-query) | `5.91.2` | `5.91.3` |
| [@tanstack/react-router](https://github.com/TanStack/router/tree/HEAD/packages/react-router) | `1.167.5` | `1.168.1` |
| [@tanstack/react-router-ssr-query](https://github.com/TanStack/router/tree/HEAD/packages/react-router-ssr-query) | `1.166.9` | `1.166.10` |
| [@tanstack/react-start](https://github.com/TanStack/router/tree/HEAD/packages/react-start) | `1.166.18` | `1.167.1` |
| [@hey-api/openapi-ts](https://github.com/hey-api/openapi-ts) | `0.94.3` | `0.94.4` |
| [oxlint-tsgolint](https://github.com/oxc-project/tsgolint) | `0.17.0` | `0.17.1` |


Updates `@tanstack/react-query` from 5.91.2 to 5.91.3
- [Release notes](https://github.com/TanStack/query/releases)
- [Changelog](https://github.com/TanStack/query/blob/main/packages/react-query/CHANGELOG.md)
- [Commits](https://github.com/TanStack/query/commits/@tanstack/react-query@5.91.3/packages/react-query)

Updates `@tanstack/react-router` from 1.167.5 to 1.168.1
- [Release notes](https://github.com/TanStack/router/releases)
- [Changelog](https://github.com/TanStack/router/blob/main/packages/react-router/CHANGELOG.md)
- [Commits](https://github.com/TanStack/router/commits/@tanstack/react-router@1.168.1/packages/react-router)

Updates `@tanstack/react-router-ssr-query` from 1.166.9 to 1.166.10
- [Release notes](https://github.com/TanStack/router/releases)
- [Changelog](https://github.com/TanStack/router/blob/main/packages/react-router-ssr-query/CHANGELOG.md)
- [Commits](https://github.com/TanStack/router/commits/@tanstack/react-router-ssr-query@1.166.10/packages/react-router-ssr-query)

Updates `@tanstack/react-start` from 1.166.18 to 1.167.1
- [Release notes](https://github.com/TanStack/router/releases)
- [Changelog](https://github.com/TanStack/router/blob/main/packages/react-start/CHANGELOG.md)
- [Commits](https://github.com/TanStack/router/commits/@tanstack/react-start@1.167.1/packages/react-start)

Updates `@hey-api/openapi-ts` from 0.94.3 to 0.94.4
- [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.94.3...@hey-api/openapi-ts@0.94.4)

Updates `oxlint-tsgolint` from 0.17.0 to 0.17.1
- [Release notes](https://github.com/oxc-project/tsgolint/releases)
- [Commits](https://github.com/oxc-project/tsgolint/compare/v0.17.0...v0.17.1)

---
updated-dependencies:
- dependency-name: "@tanstack/react-query"
  dependency-version: 5.91.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@tanstack/react-router"
  dependency-version: 1.168.1
  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.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@tanstack/react-start"
  dependency-version: 1.167.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: "@hey-api/openapi-ts"
  dependency-version: 0.94.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: oxlint-tsgolint
  dependency-version: 0.17.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-21 20:47:47 +01:00
Nico
fa8e57a589
fix: close read line interfaces on spawn errors (#693) 2026-03-21 20:46:22 +01:00
Nico
3c9620f973
fix(rclone): correctly propagate RCLONE_NO_CHECK_CERTIFICATE env variable (#692)
Closes #688
2026-03-21 20:42:22 +01:00
Nico
618f37e250
fix(notifications): fix telegram shoutrrr URL encoding (#691)
Closes #690
2026-03-21 20:38:45 +01:00
Nicolas Meienberger
aa23967329 fix: encrypt ntfy access token at rest
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2026-03-20 20:37:51 +01:00
Nicolas Meienberger
03739f9799 chore: bump rclone to 1.73.2
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2026-03-20 19:48:34 +01:00
dependabot[bot]
94cd52dec7
chore(deps): bump the minor-patch group across 1 directory with 20 updates (#681)
Bumps the minor-patch group with 20 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@inquirer/prompts](https://github.com/SBoudrias/Inquirer.js) | `8.3.0` | `8.3.2` |
| [@scalar/hono-api-reference](https://github.com/scalar/scalar/tree/HEAD/integrations/hono) | `0.10.3` | `0.10.4` |
| [@tanstack/react-hotkeys](https://github.com/TanStack/hotkeys/tree/HEAD/packages/react-hotkeys) | `0.4.1` | `0.4.2` |
| [@tanstack/react-query](https://github.com/TanStack/query/tree/HEAD/packages/react-query) | `5.90.21` | `5.91.2` |
| [@tanstack/react-router](https://github.com/TanStack/router/tree/HEAD/packages/react-router) | `1.166.8` | `1.167.5` |
| [@tanstack/react-router-ssr-query](https://github.com/TanStack/router/tree/HEAD/packages/react-router-ssr-query) | `1.166.7` | `1.166.9` |
| [@tanstack/react-start](https://github.com/TanStack/router/tree/HEAD/packages/react-start) | `1.166.9` | `1.166.18` |
| [hono](https://github.com/honojs/hono) | `4.12.7` | `4.12.8` |
| [slugify](https://github.com/simov/slugify) | `1.6.6` | `1.6.8` |
| [@hey-api/openapi-ts](https://github.com/hey-api/openapi-ts) | `0.94.1` | `0.94.3` |
| [@libsql/client](https://github.com/tursodatabase/libsql-client-ts/tree/HEAD/packages/libsql-client) | `0.17.0` | `0.17.2` |
| [@rolldown/plugin-babel](https://github.com/rolldown/plugins/tree/HEAD/packages/babel) | `0.2.1` | `0.2.2` |
| [@tailwindcss/vite](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-vite) | `4.2.1` | `4.2.2` |
| [@types/bun](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/bun) | `1.3.10` | `1.3.11` |
| [oxfmt](https://github.com/oxc-project/oxc/tree/HEAD/npm/oxfmt) | `0.40.0` | `0.41.0` |
| [oxlint](https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint) | `1.55.0` | `1.56.0` |
| [oxlint-tsgolint](https://github.com/oxc-project/tsgolint) | `0.16.0` | `0.17.0` |
| [tailwindcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss) | `4.2.1` | `4.2.2` |
| [turbo](https://github.com/vercel/turborepo) | `2.8.17` | `2.8.20` |
| [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `8.0.0` | `8.0.1` |



Updates `@inquirer/prompts` from 8.3.0 to 8.3.2
- [Release notes](https://github.com/SBoudrias/Inquirer.js/releases)
- [Commits](https://github.com/SBoudrias/Inquirer.js/compare/@inquirer/prompts@8.3.0...@inquirer/prompts@8.3.2)

Updates `@scalar/hono-api-reference` from 0.10.3 to 0.10.4
- [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.4.1 to 0.4.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.4.2/packages/react-hotkeys)

Updates `@tanstack/react-query` from 5.90.21 to 5.91.2
- [Release notes](https://github.com/TanStack/query/releases)
- [Changelog](https://github.com/TanStack/query/blob/main/packages/react-query/CHANGELOG.md)
- [Commits](https://github.com/TanStack/query/commits/@tanstack/react-query@5.91.2/packages/react-query)

Updates `@tanstack/react-router` from 1.166.8 to 1.167.5
- [Release notes](https://github.com/TanStack/router/releases)
- [Changelog](https://github.com/TanStack/router/blob/main/packages/react-router/CHANGELOG.md)
- [Commits](https://github.com/TanStack/router/commits/@tanstack/react-router@1.167.5/packages/react-router)

Updates `@tanstack/react-router-ssr-query` from 1.166.7 to 1.166.9
- [Release notes](https://github.com/TanStack/router/releases)
- [Changelog](https://github.com/TanStack/router/blob/main/packages/react-router-ssr-query/CHANGELOG.md)
- [Commits](https://github.com/TanStack/router/commits/@tanstack/react-router-ssr-query@1.166.9/packages/react-router-ssr-query)

Updates `@tanstack/react-start` from 1.166.9 to 1.166.18
- [Release notes](https://github.com/TanStack/router/releases)
- [Changelog](https://github.com/TanStack/router/blob/main/packages/react-start/CHANGELOG.md)
- [Commits](https://github.com/TanStack/router/commits/@tanstack/react-start@1.166.18/packages/react-start)

Updates `hono` from 4.12.7 to 4.12.8
- [Release notes](https://github.com/honojs/hono/releases)
- [Commits](https://github.com/honojs/hono/compare/v4.12.7...v4.12.8)

Updates `slugify` from 1.6.6 to 1.6.8
- [Changelog](https://github.com/simov/slugify/blob/master/CHANGELOG.md)
- [Commits](https://github.com/simov/slugify/compare/v1.6.6...v1.6.8)

Updates `@hey-api/openapi-ts` from 0.94.1 to 0.94.3
- [Release notes](https://github.com/hey-api/openapi-ts/releases)
- [Changelog](https://github.com/hey-api/openapi-ts/blob/main/docs/CHANGELOG.md)
- [Commits](https://github.com/hey-api/openapi-ts/compare/@hey-api/openapi-ts@0.94.1...@hey-api/openapi-ts@0.94.3)

Updates `@libsql/client` from 0.17.0 to 0.17.2
- [Release notes](https://github.com/tursodatabase/libsql-client-ts/releases)
- [Changelog](https://github.com/tursodatabase/libsql-client-ts/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tursodatabase/libsql-client-ts/commits/v0.17.2/packages/libsql-client)

Updates `@rolldown/plugin-babel` from 0.2.1 to 0.2.2
- [Release notes](https://github.com/rolldown/plugins/releases)
- [Changelog](https://github.com/rolldown/plugins/blob/main/packages/babel/CHANGELOG.md)
- [Commits](https://github.com/rolldown/plugins/commits/plugin-babel@0.2.2/packages/babel)

Updates `@tailwindcss/vite` from 4.2.1 to 4.2.2
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.2.2/packages/@tailwindcss-vite)

Updates `@types/bun` from 1.3.10 to 1.3.11
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/bun)

Updates `oxfmt` from 0.40.0 to 0.41.0
- [Release notes](https://github.com/oxc-project/oxc/releases)
- [Changelog](https://github.com/oxc-project/oxc/blob/main/npm/oxfmt/CHANGELOG.md)
- [Commits](https://github.com/oxc-project/oxc/commits/oxfmt_v0.41.0/npm/oxfmt)

Updates `oxlint` from 1.55.0 to 1.56.0
- [Release notes](https://github.com/oxc-project/oxc/releases)
- [Changelog](https://github.com/oxc-project/oxc/blob/main/npm/oxlint/CHANGELOG.md)
- [Commits](https://github.com/oxc-project/oxc/commits/oxlint_v1.56.0/npm/oxlint)

Updates `oxlint-tsgolint` from 0.16.0 to 0.17.0
- [Release notes](https://github.com/oxc-project/tsgolint/releases)
- [Commits](https://github.com/oxc-project/tsgolint/compare/v0.16.0...v0.17.0)

Updates `tailwindcss` from 4.2.1 to 4.2.2
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.2.2/packages/tailwindcss)

Updates `turbo` from 2.8.17 to 2.8.20
- [Release notes](https://github.com/vercel/turborepo/releases)
- [Changelog](https://github.com/vercel/turborepo/blob/main/RELEASE.md)
- [Commits](https://github.com/vercel/turborepo/compare/v2.8.17...v2.8.20)

Updates `vite` from 8.0.0 to 8.0.1
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/create-vite@8.0.1/packages/vite)

---
updated-dependencies:
- dependency-name: "@inquirer/prompts"
  dependency-version: 8.3.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@scalar/hono-api-reference"
  dependency-version: 0.10.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@tanstack/react-hotkeys"
  dependency-version: 0.4.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@tanstack/react-query"
  dependency-version: 5.91.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: "@tanstack/react-router"
  dependency-version: 1.167.5
  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.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@tanstack/react-start"
  dependency-version: 1.166.18
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: hono
  dependency-version: 4.12.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: slugify
  dependency-version: 1.6.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@hey-api/openapi-ts"
  dependency-version: 0.94.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@libsql/client"
  dependency-version: 0.17.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@rolldown/plugin-babel"
  dependency-version: 0.2.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@tailwindcss/vite"
  dependency-version: 4.2.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@types/bun"
  dependency-version: 1.3.11
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: oxfmt
  dependency-version: 0.41.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: oxlint
  dependency-version: 1.56.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: oxlint-tsgolint
  dependency-version: 0.17.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: tailwindcss
  dependency-version: 4.2.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: turbo
  dependency-version: 2.8.20
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: vite
  dependency-version: 8.0.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-20 19:47:22 +01:00
Nicolas Meienberger
bad5f78991 fix(smb): use long form params username / password instead of user / pass
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
#674
2026-03-20 19:29:40 +01:00
Nico
a039bb478e
fix: separate raw include paths and patterns (#683)
Separate include patters and included path cleanly to avoid path with special characters to be expanded. Closes https://github.com/nicotsx/zerobyte/discussions/680

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

* **New Features**
  * Added ability to select specific directories and paths for inclusion in backup schedules, separate from pattern-based rules.

* **Bug Fixes & Improvements**
  * Automatically migrates existing backup configurations to work with the new path selection system.
  * Enhanced backup restoration to properly handle both selected paths and pattern-based inclusions.

* **Chores**
  * Updated database schema to support path selections in backup schedules.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-03-20 19:27:54 +01:00
Nico
f6f17cd61c
fix: backup progress use simple polling & cache (#678)
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
#593
2026-03-18 20:25:30 +01:00
Nico
429b69ec92
fix: show back warnings logs and surface in UI (#677)
fix: show back warnings logs and surface in UI

#544

chore: fix dev login issue
2026-03-18 20:21:14 +01:00
Nico
e39220c024
fix: allow quoted urls in env variables (#676) 2026-03-17 22:09:22 +01:00
dependabot[bot]
ee73f89863
chore(deps-dev): bump @vitejs/plugin-react from 5.1.4 to 6.0.1 (#669)
* chore(deps-dev): bump @vitejs/plugin-react from 5.1.4 to 6.0.1

Bumps [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) from 5.1.4 to 6.0.1.
- [Release notes](https://github.com/vitejs/vite-plugin-react/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-react/commits/plugin-react@6.0.1/packages/plugin-react)

---
updated-dependencies:
- dependency-name: "@vitejs/plugin-react"
  dependency-version: 6.0.1
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore: move react-compiler to rolldown babel plugin

* chore(vite): use native tsconfig path resolution

* chore: add @babel/core types dev dep

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Nicolas Meienberger <github@thisprops.com>
2026-03-15 12:48:14 +01:00
Nicolas Meienberger
4356ace665 refactor: users table 2026-03-15 12:25:07 +01:00
dependabot[bot]
59fcce7e40
chore(deps-dev): bump vite from 7.3.1 to 8.0.0 (#667)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 7.3.1 to 8.0.0.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/create-vite@8.0.0/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 8.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-15 12:24:31 +01:00
Nicolas Meienberger
a671b17b4f refactor(schedule form): helper for multiline parsing 2026-03-15 12:09:18 +01:00
Nicolas Meienberger
2d10da72f6 refactor(volume-backend): assert mounted helper 2026-03-15 12:03:27 +01:00
Nicolas Meienberger
959cb21d83 refactor(datetime): formatting helpers 2026-03-15 11:57:37 +01:00
Nicolas Meienberger
5f7f2005fa refactor(notifications): extract config secret mapping 2026-03-15 11:51:17 +01:00
Nicolas Meienberger
a2d34a027e refactor(notifications): use native URL builder 2026-03-15 11:40:03 +01:00
Nicolas Meienberger
e58f82d7ed test(e2e): fix flaky combobox volume selection 2026-03-14 16:24:42 +01:00
dependabot[bot]
1be83ccf4c
chore(deps): bump the minor-patch group with 5 updates (#665)
Bumps the minor-patch group with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [@scalar/hono-api-reference](https://github.com/scalar/scalar/tree/HEAD/integrations/hono) | `0.10.2` | `0.10.3` |
| [@tanstack/react-router](https://github.com/TanStack/router/tree/HEAD/packages/react-router) | `1.166.7` | `1.166.8` |
| [@tanstack/react-start](https://github.com/TanStack/router/tree/HEAD/packages/react-start) | `1.166.8` | `1.166.9` |
| [@happy-dom/global-registrator](https://github.com/capricorn86/happy-dom) | `20.8.3` | `20.8.4` |
| [turbo](https://github.com/vercel/turborepo) | `2.8.16` | `2.8.17` |


Updates `@scalar/hono-api-reference` from 0.10.2 to 0.10.3
- [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-router` from 1.166.7 to 1.166.8
- [Release notes](https://github.com/TanStack/router/releases)
- [Changelog](https://github.com/TanStack/router/blob/main/packages/react-router/CHANGELOG.md)
- [Commits](https://github.com/TanStack/router/commits/@tanstack/react-router@1.166.8/packages/react-router)

Updates `@tanstack/react-start` from 1.166.8 to 1.166.9
- [Release notes](https://github.com/TanStack/router/releases)
- [Changelog](https://github.com/TanStack/router/blob/main/packages/react-start/CHANGELOG.md)
- [Commits](https://github.com/TanStack/router/commits/@tanstack/react-start@1.166.9/packages/react-start)

Updates `@happy-dom/global-registrator` from 20.8.3 to 20.8.4
- [Release notes](https://github.com/capricorn86/happy-dom/releases)
- [Commits](https://github.com/capricorn86/happy-dom/compare/v20.8.3...v20.8.4)

Updates `turbo` from 2.8.16 to 2.8.17
- [Release notes](https://github.com/vercel/turborepo/releases)
- [Changelog](https://github.com/vercel/turborepo/blob/main/RELEASE.md)
- [Commits](https://github.com/vercel/turborepo/compare/v2.8.16...v2.8.17)

---
updated-dependencies:
- dependency-name: "@scalar/hono-api-reference"
  dependency-version: 0.10.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@tanstack/react-router"
  dependency-version: 1.166.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@tanstack/react-start"
  dependency-version: 1.166.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@happy-dom/global-registrator"
  dependency-version: 20.8.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: turbo
  dependency-version: 2.8.17
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-14 12:54:43 +01:00
Nicolas Meienberger
1b5f9bf62d test(e2e): give more time for folders to load 2026-03-13 23:27:52 +01:00
Nicolas Meienberger
b298524663 test: disable rate limiting in testing 2026-03-13 23:05:14 +01:00
Nico
7473c29948
feat: protect restores that would overwrite important internal path (#663) 2026-03-13 21:33:37 +01:00
Nico
9632c77177
feat: add a trust proxy env to control whether to trust incoming x-forwarded-for headers (#662) 2026-03-13 19:08:08 +01:00
Nicolas Meienberger
8fdf858dcb docs(provisioning): remove outdated part 2026-03-13 08:46:53 +01:00
dependabot[bot]
b2a8bdaa42
chore(deps): bump the minor-patch group across 1 directory with 10 updates (#658)
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
Bumps the minor-patch group with 10 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@better-auth/sso](https://github.com/better-auth/better-auth/tree/HEAD/packages/sso) | `1.5.4` | `1.5.5` |
| [@scalar/hono-api-reference](https://github.com/scalar/scalar/tree/HEAD/integrations/hono) | `0.10.1` | `0.10.2` |
| [@tanstack/react-start](https://github.com/TanStack/router/tree/HEAD/packages/react-start) | `1.166.7` | `1.166.8` |
| [better-auth](https://github.com/better-auth/better-auth/tree/HEAD/packages/better-auth) | `1.5.4` | `1.5.5` |
| [isbot](https://github.com/omrilotan/isbot) | `5.1.35` | `5.1.36` |
| [@hey-api/openapi-ts](https://github.com/hey-api/openapi-ts) | `0.94.0` | `0.94.1` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.4.0` | `25.5.0` |
| [lefthook](https://github.com/evilmartians/lefthook) | `2.1.3` | `2.1.4` |
| [oxfmt](https://github.com/oxc-project/oxc/tree/HEAD/npm/oxfmt) | `0.37.0` | `0.40.0` |
| [oxlint](https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint) | `1.52.0` | `1.55.0` |



Updates `@better-auth/sso` from 1.5.4 to 1.5.5
- [Release notes](https://github.com/better-auth/better-auth/releases)
- [Commits](https://github.com/better-auth/better-auth/commits/v1.5.5/packages/sso)

Updates `@scalar/hono-api-reference` from 0.10.1 to 0.10.2
- [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-start` from 1.166.7 to 1.166.8
- [Release notes](https://github.com/TanStack/router/releases)
- [Changelog](https://github.com/TanStack/router/blob/main/packages/react-start/CHANGELOG.md)
- [Commits](https://github.com/TanStack/router/commits/@tanstack/react-start@1.166.8/packages/react-start)

Updates `better-auth` from 1.5.4 to 1.5.5
- [Release notes](https://github.com/better-auth/better-auth/releases)
- [Commits](https://github.com/better-auth/better-auth/commits/v1.5.5/packages/better-auth)

Updates `isbot` from 5.1.35 to 5.1.36
- [Changelog](https://github.com/omrilotan/isbot/blob/main/CHANGELOG.md)
- [Commits](https://github.com/omrilotan/isbot/commits/v5.1.36)

Updates `@hey-api/openapi-ts` from 0.94.0 to 0.94.1
- [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.94.0...@hey-api/openapi-ts@0.94.1)

Updates `@types/node` from 25.4.0 to 25.5.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `lefthook` from 2.1.3 to 2.1.4
- [Release notes](https://github.com/evilmartians/lefthook/releases)
- [Changelog](https://github.com/evilmartians/lefthook/blob/master/CHANGELOG.md)
- [Commits](https://github.com/evilmartians/lefthook/compare/v2.1.3...v2.1.4)

Updates `oxfmt` from 0.37.0 to 0.40.0
- [Release notes](https://github.com/oxc-project/oxc/releases)
- [Changelog](https://github.com/oxc-project/oxc/blob/main/npm/oxfmt/CHANGELOG.md)
- [Commits](https://github.com/oxc-project/oxc/commits/oxfmt_v0.40.0/npm/oxfmt)

Updates `oxlint` from 1.52.0 to 1.55.0
- [Release notes](https://github.com/oxc-project/oxc/releases)
- [Changelog](https://github.com/oxc-project/oxc/blob/main/npm/oxlint/CHANGELOG.md)
- [Commits](https://github.com/oxc-project/oxc/commits/oxlint_v1.55.0/npm/oxlint)

---
updated-dependencies:
- dependency-name: "@better-auth/sso"
  dependency-version: 1.5.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@scalar/hono-api-reference"
  dependency-version: 0.10.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@tanstack/react-start"
  dependency-version: 1.166.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: better-auth
  dependency-version: 1.5.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: isbot
  dependency-version: 5.1.36
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@hey-api/openapi-ts"
  dependency-version: 0.94.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@types/node"
  dependency-version: 25.5.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: lefthook
  dependency-version: 2.1.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: oxfmt
  dependency-version: 0.40.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: oxlint
  dependency-version: 1.55.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-12 23:08:06 +01:00
Nicolas Meienberger
bb7d650bcd chore: default values in schemas
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2026-03-12 22:34:00 +01:00
Nicolas Meienberger
66f95dcd8e fix: reject include patterns that are not in the volume root
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2026-03-12 20:17:40 +01:00
Nicolas Meienberger
469f0d6c4e fix(dump): content-disposition header with special characters 2026-03-12 19:17:32 +01:00
Nicolas Meienberger
1fb4f90d50 docs: remove outdated part 2026-03-12 18:39:12 +01:00
Nico
d74f516336
feat: file-based provisionning of volumes and repos (#656)
* feat: file-based provisionning of volumes and repos

docs: provisioning example

chore: ui improvements

* chore: ci issues
2026-03-12 18:31:42 +01:00
Nicolas Meienberger
f7f56c6c83 refactor: repo error state 2026-03-12 18:02:05 +01:00
Nicolas Meienberger
2923bf9f27 feat: add toggle to switch snapshot order 2026-03-12 18:02:02 +01:00
Nicolas Meienberger
2da4823ee7 test(e2e): delete, re-tag, download snapshot 2026-03-12 18:01:59 +01:00
Nicolas Meienberger
b132ad6df5 fix: add additional chmod after file write
Closes #478 The mode option of fs.writeFile does not reliably apply
permissions on all filesystems. On Synology NAS Docker volumes (btrfs),
the file ends up with 0755 permissions instead of 0600, causing SSH to
refuse the key.
2026-03-12 18:01:45 +01:00
Nicolas Meienberger
a1b2d97dbc fix(restic): treat all flag-like snap id args as positional args 2026-03-12 18:01:33 +01:00
Nico
332e5bffda
refactor: extract restic in core package (#651)
* refactor: extract restic in core package

* chore: add turbo task runner

* refactor: split server utils

* chore: simplify withDeps signature and fix non-null assertion
2026-03-11 21:56:07 +01:00
Nicolas Meienberger
9e616ca35a fix: add missing tzdata package
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
Fix timezone not being correctly picked up due to missing package
2026-03-11 19:27:40 +01:00
dependabot[bot]
4e29295f68
chore(deps): bump the minor-patch group across 1 directory with 12 updates (#648)
* chore(deps): bump the minor-patch group across 1 directory with 12 updates

Bumps the minor-patch group with 12 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@scalar/hono-api-reference](https://github.com/scalar/scalar/tree/HEAD/integrations/hono) | `0.10.0` | `0.10.1` |
| [@tanstack/react-hotkeys](https://github.com/TanStack/hotkeys/tree/HEAD/packages/react-hotkeys) | `0.3.2` | `0.4.1` |
| [@tanstack/react-router](https://github.com/TanStack/router/tree/HEAD/packages/react-router) | `1.166.2` | `1.166.7` |
| [@tanstack/react-router-ssr-query](https://github.com/TanStack/router/tree/HEAD/packages/react-router-ssr-query) | `1.166.2` | `1.166.7` |
| [@tanstack/react-start](https://github.com/TanStack/router/tree/HEAD/packages/react-start) | `1.166.2` | `1.166.7` |
| [hono](https://github.com/honojs/hono) | `4.12.5` | `4.12.7` |
| [recharts](https://github.com/recharts/recharts) | `3.7.0` | `3.8.0` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.3.5` | `25.4.0` |
| [lefthook](https://github.com/evilmartians/lefthook) | `2.1.2` | `2.1.3` |
| [lightningcss](https://github.com/parcel-bundler/lightningcss) | `1.31.1` | `1.32.0` |
| [oxfmt](https://github.com/oxc-project/oxc/tree/HEAD/npm/oxfmt) | `0.36.0` | `0.37.0` |
| [oxlint](https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint) | `1.51.0` | `1.52.0` |



Updates `@scalar/hono-api-reference` from 0.10.0 to 0.10.1
- [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.2 to 0.4.1
- [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.4.1/packages/react-hotkeys)

Updates `@tanstack/react-router` from 1.166.2 to 1.166.7
- [Release notes](https://github.com/TanStack/router/releases)
- [Commits](https://github.com/TanStack/router/commits/v1.166.7/packages/react-router)

Updates `@tanstack/react-router-ssr-query` from 1.166.2 to 1.166.7
- [Release notes](https://github.com/TanStack/router/releases)
- [Commits](https://github.com/TanStack/router/commits/v1.166.7/packages/react-router-ssr-query)

Updates `@tanstack/react-start` from 1.166.2 to 1.166.7
- [Release notes](https://github.com/TanStack/router/releases)
- [Commits](https://github.com/TanStack/router/commits/v1.166.7/packages/react-start)

Updates `hono` from 4.12.5 to 4.12.7
- [Release notes](https://github.com/honojs/hono/releases)
- [Commits](https://github.com/honojs/hono/compare/v4.12.5...v4.12.7)

Updates `recharts` from 3.7.0 to 3.8.0
- [Release notes](https://github.com/recharts/recharts/releases)
- [Changelog](https://github.com/recharts/recharts/blob/main/CHANGELOG.md)
- [Commits](https://github.com/recharts/recharts/compare/v3.7.0...v3.8.0)

Updates `@types/node` from 25.3.5 to 25.4.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `lefthook` from 2.1.2 to 2.1.3
- [Release notes](https://github.com/evilmartians/lefthook/releases)
- [Changelog](https://github.com/evilmartians/lefthook/blob/master/CHANGELOG.md)
- [Commits](https://github.com/evilmartians/lefthook/compare/v2.1.2...v2.1.3)

Updates `lightningcss` from 1.31.1 to 1.32.0
- [Release notes](https://github.com/parcel-bundler/lightningcss/releases)
- [Commits](https://github.com/parcel-bundler/lightningcss/commits/v1.32.0)

Updates `oxfmt` from 0.36.0 to 0.37.0
- [Release notes](https://github.com/oxc-project/oxc/releases)
- [Changelog](https://github.com/oxc-project/oxc/blob/main/npm/oxfmt/CHANGELOG.md)
- [Commits](https://github.com/oxc-project/oxc/commits/crates_v0.37.0/npm/oxfmt)

Updates `oxlint` from 1.51.0 to 1.52.0
- [Release notes](https://github.com/oxc-project/oxc/releases)
- [Changelog](https://github.com/oxc-project/oxc/blob/main/npm/oxlint/CHANGELOG.md)
- [Commits](https://github.com/oxc-project/oxc/commits/HEAD/npm/oxlint)

---
updated-dependencies:
- dependency-name: "@scalar/hono-api-reference"
  dependency-version: 0.10.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@tanstack/react-hotkeys"
  dependency-version: 0.4.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: "@tanstack/react-router"
  dependency-version: 1.166.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@tanstack/react-router-ssr-query"
  dependency-version: 1.166.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@tanstack/react-start"
  dependency-version: 1.166.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: hono
  dependency-version: 4.12.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: recharts
  dependency-version: 3.8.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: "@types/node"
  dependency-version: 25.4.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: lefthook
  dependency-version: 2.1.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: lightningcss
  dependency-version: 1.32.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: oxfmt
  dependency-version: 0.37.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: oxlint
  dependency-version: 1.52.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore: gen api-client

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Nicolas Meienberger <github@thisprops.com>
2026-03-11 19:19:24 +01:00
Nicolas Meienberger
d07be16fc6 fix(rclone): use correct timeout in seconds 2026-03-11 19:19:06 +01:00
Nicolas Meienberger
f6169ab27e Merge branch 'bcrooker-features/show-estimated-time2' 2026-03-11 19:06:29 +01:00
Nicolas Meienberger
b156d6154e fix: rebase issues 2026-03-11 19:05:50 +01:00
Nico
e8eae7dce6 fix: dump snapshot (#646)
Closes #602
2026-03-11 18:53:58 +01:00
Nicolas Meienberger
d59677e322 chore: color improvements in light mode 2026-03-11 18:53:58 +01:00
Nico
55dbcf0eb3 refactor: move from arktype to zod (#637)
chore: gen api-client
2026-03-11 18:53:58 +01:00
Nico
b35a20f43d feat: light theme (#638) 2026-03-11 18:53:15 +01:00
dependabot[bot]
a4cc82f526 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] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-11 18:53:15 +01:00
dependabot[bot]
bdea445e4f 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] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-11 18:53:15 +01:00
dependabot[bot]
49e98d522f 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] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-11 18:53:15 +01:00
Nicolas Meienberger
b72d0af17d docs(readme): bump version 2026-03-11 18:53:15 +01:00
Nico
1b948ed41e fix: single item file restore (#634)
fix: single item file restore

test(e2e): restore single file at custom location  
  
Closes #633
2026-03-11 18:53:15 +01:00
Nicolas Meienberger
d640ad88b2 refactor: harden cron validation and fail early in case of wrong pattern 2026-03-11 18:53:15 +01:00
Vutsal
3fa793bef6 Refactor scheduler to use CronExpressionParser 2026-03-11 18:53:15 +01:00
dependabot[bot]
7fde07f107 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] <support@github.com>

* chore: gen api-client

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Nicolas Meienberger <github@thisprops.com>
2026-03-11 18:53:15 +01:00
Nico
19b827c049
fix: dump snapshot (#646)
Closes #602
2026-03-10 18:01:51 +01:00
Nicolas Meienberger
ab25cc915e chore: color improvements in light mode
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2026-03-09 19:05:51 +01:00
Nico
df312cd419
refactor: move from arktype to zod (#637)
chore: gen api-client
2026-03-09 18:27:03 +01:00
Nico
dd8f471d60
feat: light theme (#638) 2026-03-09 18:16:50 +01:00
dependabot[bot]
8a10266960
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] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-08 15:12:15 +01:00
dependabot[bot]
3131d140fa
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] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-08 15:11:54 +01:00
dependabot[bot]
1776bd85d8
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] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-08 15:11:38 +01:00
Nicolas Meienberger
57313fee9b docs(readme): bump version 2026-03-08 15:03:39 +01:00
Nico
d660c8e2e4
fix: single item file restore (#634)
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
fix: single item file restore

test(e2e): restore single file at custom location  
  
Closes #633
2026-03-08 13:38:00 +01:00
Nicolas Meienberger
3d2900ab69 refactor: harden cron validation and fail early in case of wrong pattern 2026-03-08 12:47:19 +01:00
Nicolas Meienberger
938680bb80 Merge branch 'vutsalsinghal-patch-1' 2026-03-08 08:43:29 +01:00
Vutsal
4d8a0b28ca
Refactor scheduler to use CronExpressionParser 2026-03-07 21:51:28 -08:00
bcrooker
2adec5a999 fix: add seconds_remaining to restic backup progress schema 2026-03-07 11:22:41 -05:00
dependabot[bot]
2ccaf724bf
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] <support@github.com>

* chore: gen api-client

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Nicolas Meienberger <github@thisprops.com>
2026-03-07 06:38:44 +01:00
bcrooker
8e24c6a8d8 feat(backups): show estimated time remaining on backup progress 2026-03-05 20:06:00 -05:00
Nico
d5da6d4562
fix: sse connection (#623)
### TL;DR

Refactored `useServerEvents` hook to use a shared EventSource connection across all components instead of creating individual connections per hook instance.

### What changed?

- Introduced shared state management for EventSource connections with reference counting
- Added comprehensive test coverage with MockEventSource implementation
- Modified the hook to share a single EventSource connection across multiple consumers
- Implemented proper cleanup when the last subscriber unmounts
- Removed automatic query refetching on backup completion, keeping only cache invalidation

### How to test?

Run the new test suite with `bun test use-server-events.test.tsx` to verify:
- Single EventSource instance is shared across multiple hook consumers
- Event listeners work correctly with the shared connection
- Cache invalidation occurs once per event
- Proper cleanup happens when all subscribers unmount

### Why make this change?

This optimization reduces resource usage by preventing multiple EventSource connections when the hook is used in different components. The shared connection approach is more efficient while maintaining the same functionality, and the added tests ensure reliability of the server events system.
2026-03-05 22:32:44 +01:00
Nico
3e50e37e02
chore: bump drizzle to beta-16 (#622)
chore: bump drizzle to beta-16

test: increase test coverage
2026-03-05 22:23:45 +01:00
Nicolas Meienberger
c2ed9e3693 refactor: correctly delete orphan sessions after idp deletion
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2026-03-05 21:02:56 +01:00
Nico
c1e8036323
fix: sso cross org link with valid invite (#621) 2026-03-05 20:21:24 +01:00
Nico
6769f5fa6c
fix: run repo stats in background (#620)
Closes #614
2026-03-05 20:18:34 +01:00
dependabot[bot]
addeab0e30
chore(deps): bump docker/login-action from 3 to 4 (#618)
Bumps [docker/login-action](https://github.com/docker/login-action) from 3 to 4.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-05 18:52:01 +01:00
Nicolas Meienberger
3b94794aa3 test(e2e): more cases for oidc 2026-03-04 22:33:10 +01:00
Nicolas Meienberger
6f560ecae9 chore: api client gen
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2026-03-04 22:00:04 +01:00
Nicolas Meienberger
e8ae8812ff Merge branch 'bcrooker-main'
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2026-03-04 19:42:54 +01:00
Nicolas Meienberger
157be2d6ab feat: custom flags allowlist 2026-03-04 19:42:08 +01:00
Nico
0c20473e30 refactor(sso): extract sso code into it's own module (#617) 2026-03-04 18:48:47 +01:00
Nico
2f253d4999 fix: move active session to an existing org, when deleting (#612)
### TL;DR

Added session cleanup logic to handle active organization reassignment when user organizations are deleted.

### What changed?

Enhanced the `cleanupUserOrganizations` method in `AuthService` to reassign active organizations for users whose current active organization is being deleted. The method now:

- Identifies users who are members of organizations being deleted
- Finds alternative organizations for each affected user
- Updates their sessions to use a fallback organization or null if no alternatives exist
- Wraps the entire operation in a database transaction for consistency

### How to test?

Run the new test suite:
```bash
bun test app/server/modules/auth/__tests__/auth.cleanup-user-organizations.test.ts
```

The test verifies that when a user's organization is deleted, other members' sessions are properly updated to use their remaining organization memberships as the active organization.

### Why make this change?

Prevents orphaned session references when organizations are deleted. Without this change, users could have sessions pointing to non-existent organizations as their active workspace, leading to potential application errors or inconsistent state.

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

## Release Notes

* **Bug Fixes**
  * Improved organization deletion handling. When an organization is deleted, user sessions are now automatically reassigned to a valid fallback organization, ensuring session state consistency and preventing invalid organization references.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-03-04 18:48:47 +01:00
Nico
fc11432b87 feat: change email by cli (#611)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

* **New Features**
  * New CLI command to change user email addresses with impact preview and validation.

* **Accessibility**
  * Improved settings form with proper label-input associations and email field type validation.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-03-04 18:48:47 +01:00
dependabot[bot]
371ca270fa chore(deps): bump the minor-patch group across 1 directory with 17 updates (#609)
Bumps the minor-patch group with 17 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@scalar/hono-api-reference](https://github.com/scalar/scalar/tree/HEAD/integrations/hono) | `0.9.45` | `0.9.48` |
| [@tanstack/react-hotkeys](https://github.com/TanStack/hotkeys/tree/HEAD/packages/react-hotkeys) | `0.3.0` | `0.3.1` |
| [@tanstack/react-router](https://github.com/TanStack/router/tree/HEAD/packages/react-router) | `1.162.9` | `1.163.3` |
| [@tanstack/react-router-ssr-query](https://github.com/TanStack/router/tree/HEAD/packages/react-router-ssr-query) | `1.162.9` | `1.163.3` |
| [@tanstack/react-start](https://github.com/TanStack/router/tree/HEAD/packages/react-start) | `1.162.9` | `1.166.1` |
| [es-toolkit](https://github.com/toss/es-toolkit) | `1.44.0` | `1.45.0` |
| [hono](https://github.com/honojs/hono) | `4.12.2` | `4.12.4` |
| [hono-openapi](https://github.com/rhinobase/hono-openapi) | `1.2.0` | `1.3.0` |
| [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `0.575.0` | `0.576.0` |
| [@happy-dom/global-registrator](https://github.com/capricorn86/happy-dom) | `20.7.0` | `20.8.3` |
| [@hey-api/openapi-ts](https://github.com/hey-api/openapi-ts) | `0.93.0` | `0.93.1` |
| [@types/bun](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/bun) | `1.3.9` | `1.3.10` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.3.0` | `25.3.3` |
| [lefthook](https://github.com/evilmartians/lefthook) | `2.1.1` | `2.1.2` |
| [oxfmt](https://github.com/oxc-project/oxc/tree/HEAD/npm/oxfmt) | `0.35.0` | `0.36.0` |
| [oxlint](https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint) | `1.50.0` | `1.51.0` |
| [oxlint-tsgolint](https://github.com/oxc-project/tsgolint) | `0.15.0` | `0.16.0` |



Updates `@scalar/hono-api-reference` from 0.9.45 to 0.9.48
- [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.0 to 0.3.1
- [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.1/packages/react-hotkeys)

Updates `@tanstack/react-router` from 1.162.9 to 1.163.3
- [Release notes](https://github.com/TanStack/router/releases)
- [Commits](https://github.com/TanStack/router/commits/v1.163.3/packages/react-router)

Updates `@tanstack/react-router-ssr-query` from 1.162.9 to 1.163.3
- [Release notes](https://github.com/TanStack/router/releases)
- [Commits](https://github.com/TanStack/router/commits/v1.163.3/packages/react-router-ssr-query)

Updates `@tanstack/react-start` from 1.162.9 to 1.166.1
- [Release notes](https://github.com/TanStack/router/releases)
- [Commits](https://github.com/TanStack/router/commits/v1.166.1/packages/react-start)

Updates `es-toolkit` from 1.44.0 to 1.45.0
- [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.44.0...v1.45.0)

Updates `hono` from 4.12.2 to 4.12.4
- [Release notes](https://github.com/honojs/hono/releases)
- [Commits](https://github.com/honojs/hono/compare/v4.12.2...v4.12.4)

Updates `hono-openapi` from 1.2.0 to 1.3.0
- [Release notes](https://github.com/rhinobase/hono-openapi/releases)
- [Commits](https://github.com/rhinobase/hono-openapi/compare/v1.2.0...v1.3.0)

Updates `lucide-react` from 0.575.0 to 0.576.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/0.576.0/packages/lucide-react)

Updates `@happy-dom/global-registrator` from 20.7.0 to 20.8.3
- [Release notes](https://github.com/capricorn86/happy-dom/releases)
- [Commits](https://github.com/capricorn86/happy-dom/compare/v20.7.0...v20.8.3)

Updates `@hey-api/openapi-ts` from 0.93.0 to 0.93.1
- [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.0...@hey-api/openapi-ts@0.93.1)

Updates `@types/bun` from 1.3.9 to 1.3.10
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/bun)

Updates `@types/node` from 25.3.0 to 25.3.3
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `lefthook` from 2.1.1 to 2.1.2
- [Release notes](https://github.com/evilmartians/lefthook/releases)
- [Changelog](https://github.com/evilmartians/lefthook/blob/master/CHANGELOG.md)
- [Commits](https://github.com/evilmartians/lefthook/compare/v2.1.1...v2.1.2)

Updates `oxfmt` from 0.35.0 to 0.36.0
- [Release notes](https://github.com/oxc-project/oxc/releases)
- [Changelog](https://github.com/oxc-project/oxc/blob/main/npm/oxfmt/CHANGELOG.md)
- [Commits](https://github.com/oxc-project/oxc/commits/oxfmt_v0.36.0/npm/oxfmt)

Updates `oxlint` from 1.50.0 to 1.51.0
- [Release notes](https://github.com/oxc-project/oxc/releases)
- [Changelog](https://github.com/oxc-project/oxc/blob/main/npm/oxlint/CHANGELOG.md)
- [Commits](https://github.com/oxc-project/oxc/commits/oxlint_v1.51.0/npm/oxlint)

Updates `oxlint-tsgolint` from 0.15.0 to 0.16.0
- [Release notes](https://github.com/oxc-project/tsgolint/releases)
- [Commits](https://github.com/oxc-project/tsgolint/compare/v0.15.0...v0.16.0)

---
updated-dependencies:
- dependency-name: "@scalar/hono-api-reference"
  dependency-version: 0.9.48
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@tanstack/react-hotkeys"
  dependency-version: 0.3.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@tanstack/react-router"
  dependency-version: 1.163.3
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: "@tanstack/react-router-ssr-query"
  dependency-version: 1.163.3
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: "@tanstack/react-start"
  dependency-version: 1.166.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: es-toolkit
  dependency-version: 1.45.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: hono
  dependency-version: 4.12.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: hono-openapi
  dependency-version: 1.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: lucide-react
  dependency-version: 0.576.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: "@happy-dom/global-registrator"
  dependency-version: 20.8.3
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: "@hey-api/openapi-ts"
  dependency-version: 0.93.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@types/bun"
  dependency-version: 1.3.10
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@types/node"
  dependency-version: 25.3.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: lefthook
  dependency-version: 2.1.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: oxfmt
  dependency-version: 0.36.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: oxlint
  dependency-version: 1.51.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: oxlint-tsgolint
  dependency-version: 0.16.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-04 18:48:47 +01:00
Nico
a24c6d825e refactor: sso utils (#608) 2026-03-04 18:48:47 +01:00
Nico
10303b6596 fix(use-mobile): use sync external storage to wait before initial render (#610) 2026-03-04 18:48:47 +01:00
Nico
235706b230 fix(sso): prevent auto-linking with an existing account in a different org (#607)
* fix(sso): prevent auto-linking with an existing account in a different org

* chore: lint issue

* chore: bump @better-auth/sso
2026-03-04 18:48:47 +01:00
Nicolas Meienberger
4ed554c039 chore: web-haptics 2026-03-04 18:48:47 +01:00
Nicolas Meienberger
5f6409e0a5 test: increase coverage for existing controllers 2026-03-04 18:48:47 +01:00
Nicolas Meienberger
1724429fb0 refactor: ensure server preloading 2026-03-04 18:48:47 +01:00
dependabot[bot]
1da4671133 chore(deps): bump actions/upload-artifact from 6 to 7 (#587)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 6 to 7.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-04 18:48:47 +01:00
Nico
c606099104 test: use better-auth built-in test plugin (#599)
test: use better-auth built-in test plugin

refactor: map auth errors server side

refactor: native trusted providers callback usage

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

## Release Notes

* **New Features**
  * Enhanced SSO authentication error messaging with specific guidance for different failure scenarios (account linking required, email verification needed, banned accounts, invite-only access).

* **Chores**
  * Updated authentication dependencies to version 1.5.0.

* **Tests**
  * Extended test coverage for SSO error code handling and authentication scenarios.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-03-04 18:48:47 +01:00
Nicolas Meienberger
e0eb490636 refactor: add error components at page level 2026-03-04 18:48:47 +01:00
Nicolas Meienberger
d07306cb86 chore: improve ssr data loading 2026-03-04 18:48:47 +01:00
Nicolas Meienberger
4a032d744b test(e2e): fix folder permission issue 2026-03-04 18:48:47 +01:00
Nico
8691cf8c1f style: improve repo details layout (#596) 2026-03-04 18:48:47 +01:00
Nico
c5aacd30e3 feat: separate global admin page (#595) 2026-03-04 18:48:47 +01:00
Nicolas Meienberger
812533394b docs(readme): update version 2026-03-04 18:48:47 +01:00
Nicolas Meienberger
7881f818ae test: *.xyz include pattern 2026-03-04 18:48:47 +01:00
Nicolas Meienberger
58fee9914b refactor: render repo stats directly in ssr if cache is available 2026-03-04 18:48:47 +01:00
Nicolas Meienberger
70352e9f91 refactor: render cached data directly if available during ssr 2026-03-04 18:48:47 +01:00
Nico
12454ee401 fix: restic glob pattern in include (#594)
Closes #590

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

* **Tests**
  * Enhanced test coverage for backup pattern handling and anchoring
  * Added end-to-end scenarios validating include patterns, exclusion patterns, and exclude-if-present functionality

* **Refactor**
  * Updated pattern processing logic to improve relative path resolution for backup patterns
  * Improved asynchronous handling in backup initialization workflow

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-03-04 18:48:47 +01:00
Nico
29db09bb5b feat: OIDC (#564)
* feat: oidc

feat: organization switcher

refactor: org context

feat: invitations

GLM

* feat: link current account

* refactor: own page for sso registration

* feat: per-user account management

* refactor: code style

* refactor: user existing check

* refactor: restrict provider configuration to super admins only

* refactor: cleanup / pr review

* chore: fix lint issues

* chore: pr feedbacks

* test(e2e): automated tests for OIDC

* fix: check url first for sso provider identification

* fix: prevent oidc provider to be named "credential"
2026-03-04 18:48:00 +01:00
Nico
4a601d157d
refactor(sso): extract sso code into it's own module (#617) 2026-03-04 18:47:24 +01:00
Nico
7dc017f4b6
fix: move active session to an existing org, when deleting (#612)
### TL;DR

Added session cleanup logic to handle active organization reassignment when user organizations are deleted.

### What changed?

Enhanced the `cleanupUserOrganizations` method in `AuthService` to reassign active organizations for users whose current active organization is being deleted. The method now:

- Identifies users who are members of organizations being deleted
- Finds alternative organizations for each affected user
- Updates their sessions to use a fallback organization or null if no alternatives exist
- Wraps the entire operation in a database transaction for consistency

### How to test?

Run the new test suite:
```bash
bun test app/server/modules/auth/__tests__/auth.cleanup-user-organizations.test.ts
```

The test verifies that when a user's organization is deleted, other members' sessions are properly updated to use their remaining organization memberships as the active organization.

### Why make this change?

Prevents orphaned session references when organizations are deleted. Without this change, users could have sessions pointing to non-existent organizations as their active workspace, leading to potential application errors or inconsistent state.

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

## Release Notes

* **Bug Fixes**
  * Improved organization deletion handling. When an organization is deleted, user sessions are now automatically reassigned to a valid fallback organization, ensuring session state consistency and preventing invalid organization references.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-03-04 18:45:01 +01:00
Nico
fb3c5b33c0
feat: change email by cli (#611)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

* **New Features**
  * New CLI command to change user email addresses with impact preview and validation.

* **Accessibility**
  * Improved settings form with proper label-input associations and email field type validation.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-03-03 21:46:47 +01:00
dependabot[bot]
4bb6a52431
chore(deps): bump the minor-patch group across 1 directory with 17 updates (#609)
Bumps the minor-patch group with 17 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@scalar/hono-api-reference](https://github.com/scalar/scalar/tree/HEAD/integrations/hono) | `0.9.45` | `0.9.48` |
| [@tanstack/react-hotkeys](https://github.com/TanStack/hotkeys/tree/HEAD/packages/react-hotkeys) | `0.3.0` | `0.3.1` |
| [@tanstack/react-router](https://github.com/TanStack/router/tree/HEAD/packages/react-router) | `1.162.9` | `1.163.3` |
| [@tanstack/react-router-ssr-query](https://github.com/TanStack/router/tree/HEAD/packages/react-router-ssr-query) | `1.162.9` | `1.163.3` |
| [@tanstack/react-start](https://github.com/TanStack/router/tree/HEAD/packages/react-start) | `1.162.9` | `1.166.1` |
| [es-toolkit](https://github.com/toss/es-toolkit) | `1.44.0` | `1.45.0` |
| [hono](https://github.com/honojs/hono) | `4.12.2` | `4.12.4` |
| [hono-openapi](https://github.com/rhinobase/hono-openapi) | `1.2.0` | `1.3.0` |
| [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `0.575.0` | `0.576.0` |
| [@happy-dom/global-registrator](https://github.com/capricorn86/happy-dom) | `20.7.0` | `20.8.3` |
| [@hey-api/openapi-ts](https://github.com/hey-api/openapi-ts) | `0.93.0` | `0.93.1` |
| [@types/bun](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/bun) | `1.3.9` | `1.3.10` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.3.0` | `25.3.3` |
| [lefthook](https://github.com/evilmartians/lefthook) | `2.1.1` | `2.1.2` |
| [oxfmt](https://github.com/oxc-project/oxc/tree/HEAD/npm/oxfmt) | `0.35.0` | `0.36.0` |
| [oxlint](https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint) | `1.50.0` | `1.51.0` |
| [oxlint-tsgolint](https://github.com/oxc-project/tsgolint) | `0.15.0` | `0.16.0` |



Updates `@scalar/hono-api-reference` from 0.9.45 to 0.9.48
- [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.0 to 0.3.1
- [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.1/packages/react-hotkeys)

Updates `@tanstack/react-router` from 1.162.9 to 1.163.3
- [Release notes](https://github.com/TanStack/router/releases)
- [Commits](https://github.com/TanStack/router/commits/v1.163.3/packages/react-router)

Updates `@tanstack/react-router-ssr-query` from 1.162.9 to 1.163.3
- [Release notes](https://github.com/TanStack/router/releases)
- [Commits](https://github.com/TanStack/router/commits/v1.163.3/packages/react-router-ssr-query)

Updates `@tanstack/react-start` from 1.162.9 to 1.166.1
- [Release notes](https://github.com/TanStack/router/releases)
- [Commits](https://github.com/TanStack/router/commits/v1.166.1/packages/react-start)

Updates `es-toolkit` from 1.44.0 to 1.45.0
- [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.44.0...v1.45.0)

Updates `hono` from 4.12.2 to 4.12.4
- [Release notes](https://github.com/honojs/hono/releases)
- [Commits](https://github.com/honojs/hono/compare/v4.12.2...v4.12.4)

Updates `hono-openapi` from 1.2.0 to 1.3.0
- [Release notes](https://github.com/rhinobase/hono-openapi/releases)
- [Commits](https://github.com/rhinobase/hono-openapi/compare/v1.2.0...v1.3.0)

Updates `lucide-react` from 0.575.0 to 0.576.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/0.576.0/packages/lucide-react)

Updates `@happy-dom/global-registrator` from 20.7.0 to 20.8.3
- [Release notes](https://github.com/capricorn86/happy-dom/releases)
- [Commits](https://github.com/capricorn86/happy-dom/compare/v20.7.0...v20.8.3)

Updates `@hey-api/openapi-ts` from 0.93.0 to 0.93.1
- [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.0...@hey-api/openapi-ts@0.93.1)

Updates `@types/bun` from 1.3.9 to 1.3.10
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/bun)

Updates `@types/node` from 25.3.0 to 25.3.3
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `lefthook` from 2.1.1 to 2.1.2
- [Release notes](https://github.com/evilmartians/lefthook/releases)
- [Changelog](https://github.com/evilmartians/lefthook/blob/master/CHANGELOG.md)
- [Commits](https://github.com/evilmartians/lefthook/compare/v2.1.1...v2.1.2)

Updates `oxfmt` from 0.35.0 to 0.36.0
- [Release notes](https://github.com/oxc-project/oxc/releases)
- [Changelog](https://github.com/oxc-project/oxc/blob/main/npm/oxfmt/CHANGELOG.md)
- [Commits](https://github.com/oxc-project/oxc/commits/oxfmt_v0.36.0/npm/oxfmt)

Updates `oxlint` from 1.50.0 to 1.51.0
- [Release notes](https://github.com/oxc-project/oxc/releases)
- [Changelog](https://github.com/oxc-project/oxc/blob/main/npm/oxlint/CHANGELOG.md)
- [Commits](https://github.com/oxc-project/oxc/commits/oxlint_v1.51.0/npm/oxlint)

Updates `oxlint-tsgolint` from 0.15.0 to 0.16.0
- [Release notes](https://github.com/oxc-project/tsgolint/releases)
- [Commits](https://github.com/oxc-project/tsgolint/compare/v0.15.0...v0.16.0)

---
updated-dependencies:
- dependency-name: "@scalar/hono-api-reference"
  dependency-version: 0.9.48
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@tanstack/react-hotkeys"
  dependency-version: 0.3.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@tanstack/react-router"
  dependency-version: 1.163.3
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: "@tanstack/react-router-ssr-query"
  dependency-version: 1.163.3
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: "@tanstack/react-start"
  dependency-version: 1.166.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: es-toolkit
  dependency-version: 1.45.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: hono
  dependency-version: 4.12.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: hono-openapi
  dependency-version: 1.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: lucide-react
  dependency-version: 0.576.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: "@happy-dom/global-registrator"
  dependency-version: 20.8.3
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: "@hey-api/openapi-ts"
  dependency-version: 0.93.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@types/bun"
  dependency-version: 1.3.10
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@types/node"
  dependency-version: 25.3.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: lefthook
  dependency-version: 2.1.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: oxfmt
  dependency-version: 0.36.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: oxlint
  dependency-version: 1.51.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: oxlint-tsgolint
  dependency-version: 0.16.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-03 21:33:18 +01:00
Nico
cb405f1790
refactor: sso utils (#608) 2026-03-03 21:28:39 +01:00
Nico
cdc15ef488
fix(use-mobile): use sync external storage to wait before initial render (#610) 2026-03-03 21:27:22 +01:00
Nico
f511095fde
fix(sso): prevent auto-linking with an existing account in a different org (#607)
* fix(sso): prevent auto-linking with an existing account in a different org

* chore: lint issue

* chore: bump @better-auth/sso
2026-03-03 20:27:50 +01:00
Nicolas Meienberger
b082afe761 chore: web-haptics 2026-03-03 19:27:43 +01:00
Nicolas Meienberger
1db50e41f9 test: increase coverage for existing controllers
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2026-03-02 21:16:48 +01:00
Nicolas Meienberger
5ef2026b61 refactor: ensure server preloading 2026-03-02 21:13:32 +01:00
dependabot[bot]
5775c56bd8
chore(deps): bump actions/upload-artifact from 6 to 7 (#587)
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 6 to 7.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-01 17:20:25 +01:00
Nico
2ff6451f37
test: use better-auth built-in test plugin (#599)
test: use better-auth built-in test plugin

refactor: map auth errors server side

refactor: native trusted providers callback usage

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

## Release Notes

* **New Features**
  * Enhanced SSO authentication error messaging with specific guidance for different failure scenarios (account linking required, email verification needed, banned accounts, invite-only access).

* **Chores**
  * Updated authentication dependencies to version 1.5.0.

* **Tests**
  * Extended test coverage for SSO error code handling and authentication scenarios.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-03-01 15:10:50 +01:00
Nicolas Meienberger
e86fcf1b9a refactor: add error components at page level 2026-02-28 17:05:27 +01:00
Nicolas Meienberger
39469cb08b chore: improve ssr data loading
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2026-02-28 16:47:02 +01:00
Nicolas Meienberger
0ad4d9ab81 test(e2e): fix folder permission issue
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2026-02-28 14:57:31 +01:00
Nico
67eca589d8
style: improve repo details layout (#596) 2026-02-28 14:16:50 +01:00
Nico
f686c1aa16
feat: separate global admin page (#595) 2026-02-28 12:32:43 +01:00
Nicolas Meienberger
e6a8b83900 docs(readme): update version 2026-02-28 11:17:15 +01:00
Nicolas Meienberger
84a088a42e test: *.xyz include pattern 2026-02-28 11:16:04 +01:00
Nicolas Meienberger
73c0511167 refactor: render repo stats directly in ssr if cache is available 2026-02-28 10:17:55 +01:00
Nicolas Meienberger
66076e5841 refactor: render cached data directly if available during ssr 2026-02-28 00:59:05 +01:00
Nico
bba61c2a37
fix: restic glob pattern in include (#594)
Closes #590

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

* **Tests**
  * Enhanced test coverage for backup pattern handling and anchoring
  * Added end-to-end scenarios validating include patterns, exclusion patterns, and exclude-if-present functionality

* **Refactor**
  * Updated pattern processing logic to improve relative path resolution for backup patterns
  * Improved asynchronous handling in backup initialization workflow

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-28 00:02:00 +01:00
Nico
7a3932f969
feat: OIDC (#564)
* feat: oidc

feat: organization switcher

refactor: org context

feat: invitations

GLM

* feat: link current account

* refactor: own page for sso registration

* feat: per-user account management

* refactor: code style

* refactor: user existing check

* refactor: restrict provider configuration to super admins only

* refactor: cleanup / pr review

* chore: fix lint issues

* chore: pr feedbacks

* test(e2e): automated tests for OIDC

* fix: check url first for sso provider identification

* fix: prevent oidc provider to be named "credential"
2026-02-27 23:13:54 +01:00
bcrooker
ef51d665c8 feat(backups): add custom restic params to backup schedules
Allows users to pass arbitrary restic flags via a new advanced section in the create/edit schedule form. Includes DB migration, schema update, DTO, service, and restic command changes.
2026-02-27 14:46:47 -05:00
Nico
b2d2f28b40
style: redesign dashboard (#586)
style: redesign dashboard

style(tables): same height row in all pages
2026-02-26 19:57:39 +01:00
Nico
942bf2b31b
refactor: tests (#585)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

* **Tests**
  * Improved test infrastructure and organization across backup, repository, volume, and utility modules with streamlined mock setup and cleaner test patterns.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-26 19:54:25 +01:00
Nico
4a812a884b
refactor(restic): split each command into its own file (#584)
refactor(restic): split each command into its own file

refactor: add missing await before promise expects

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

## Release Notes

* **Tests**
  * Improved async assertion handling across test suites for enhanced test reliability
  * Expanded test coverage for backup, restore, and other core operations

* **Refactor**
  * Reorganized internal utility structure for improved code maintainability

* **Chores**
  * Updated linting configuration and TypeScript dependencies

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-26 19:51:24 +01:00
Nico
3113af8ce8
fix(repo): add a custom key after creation with the configured hostname (#583)
Closes #456
2026-02-26 19:46:25 +01:00
Nico
30f237dc1f
fix(local-repo): automatically generate a subfolder for new local repos (#582)
Closes #562

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

## Release Notes

* **New Features**
  * Improved repository setup with context-aware descriptions for new vs. existing repositories
  * Enhanced path handling: unique identifiers automatically appended for new repositories, exact paths preserved when importing existing ones

* **Chores**
  * Updated documentation and build configuration

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-26 19:43:42 +01:00
Nicolas Meienberger
b1a0cdb195 chore: gen api-client
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2026-02-25 23:19:34 +01:00
Nicolas Meienberger
907bc57c2d fix(restic-dto): allow missing current_files in schema
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2026-02-25 23:07:53 +01:00
Nicolas Meienberger
d533e470fc chore: bump rclone to 1.73.1
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2026-02-25 22:17:24 +01:00
Nicolas Meienberger
f3c753ad30 chore: style fixes 2026-02-25 22:15:08 +01:00
Nico
c959fb77cf
ci: parallelize ci checks (#577) 2026-02-25 21:18:18 +01:00
dependabot[bot]
5bc0de4d1a
chore(deps): bump the minor-patch group with 6 updates (#570)
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
* chore(deps): bump the minor-patch group with 6 updates

Bumps the minor-patch group with 6 updates:

| Package | From | To |
| --- | --- | --- |
| @scalar/hono-api-reference | `0.9.44` | `0.9.45` |
| @tanstack/react-router | `1.162.8` | `1.162.9` |
| @tanstack/react-router-ssr-query | `1.162.8` | `1.162.9` |
| @tanstack/react-start | `1.162.8` | `1.162.9` |
| @hey-api/openapi-ts | `0.92.4` | `0.93.0` |
| oxlint-tsgolint | `0.14.2` | `0.15.0` |


Updates `@scalar/hono-api-reference` from 0.9.44 to 0.9.45

Updates `@tanstack/react-router` from 1.162.8 to 1.162.9

Updates `@tanstack/react-router-ssr-query` from 1.162.8 to 1.162.9

Updates `@tanstack/react-start` from 1.162.8 to 1.162.9

Updates `@hey-api/openapi-ts` from 0.92.4 to 0.93.0

Updates `oxlint-tsgolint` from 0.14.2 to 0.15.0

---
updated-dependencies:
- dependency-name: "@scalar/hono-api-reference"
  dependency-version: 0.9.45
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@tanstack/react-router"
  dependency-version: 1.162.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@tanstack/react-router-ssr-query"
  dependency-version: 1.162.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@tanstack/react-start"
  dependency-version: 1.162.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@hey-api/openapi-ts"
  dependency-version: 0.93.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: oxlint-tsgolint
  dependency-version: 0.15.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore: gen-api client

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Nicolas Meienberger <github@thisprops.com>
2026-02-25 19:19:00 +01:00
Nico
c4f3300e9b
fix(backups): correctly resolve paths when folder name matches volume name (#576)
When a selected subfolder had the exact same name as its parent volume
mount, the `path.relative` check falsely identified the subfolder path
as an absolute path already inside the volume. This caused the entire
volume root to be backed up instead of the intended subfolder.

Closes #250

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

* **Bug Fixes**
  * Improved handling of backup patterns when folder names match volume names.
  * Enhanced error handling for snapshot deletion operations.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-25 19:09:32 +01:00
Nico
cc55fee339
refactor: increase rclone mount timeout (#575)
Closes #488
2026-02-25 19:06:07 +01:00
Nico
dd9083ce7a
fix: wrong cache key for snapshot retention tags (#574)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

* **Bug Fixes**
  * Enhanced error handling in retention category calculations to gracefully handle missing or invalid repositories, ensuring the system returns safe defaults instead of failing
  * Optimized repository cache validation and lookup mechanisms for improved performance
  * Strengthened repository validation checks to prevent potential calculation failures
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-25 19:02:44 +01:00
Nico
29967d7e4e
fix(discord): do not split each line into individual messages (#573)
Closes #519

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

* **New Features**
  * Notification destinations can now be tested even when disabled, enabling configuration validation before activation

* **Improvements**
  * Discord notification formatting parameters optimized for enhanced message delivery

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-25 18:54:56 +01:00
Nico
b916df7736
fix: form not correctly updating based on values (#572)
* fix: form not correctly updating based on values

Closes #549

* chore: pr review
2026-02-25 18:36:12 +01:00
Nico
cb92d36c95
feat: cache backup progress (#571)
Closes #412
2026-02-25 18:20:08 +01:00
Nico
00e7118771
fix: report timeout as errors (#569)
Closes #485
2026-02-25 18:02:47 +01:00
dependabot[bot]
5b8f05bdf8
chore(deps): bump the minor-patch group across 1 directory with 18 updates (#566)
Bumps the minor-patch group with 18 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@inquirer/prompts](https://github.com/SBoudrias/Inquirer.js) | `8.2.1` | `8.3.0` |
| [@scalar/hono-api-reference](https://github.com/scalar/scalar/tree/HEAD/integrations/hono) | `0.9.41` | `0.9.44` |
| [@tanstack/react-hotkeys](https://github.com/TanStack/hotkeys/tree/HEAD/packages/react-hotkeys) | `0.1.0` | `0.3.0` |
| [@tanstack/react-router](https://github.com/TanStack/router/tree/HEAD/packages/react-router) | `1.161.3` | `1.162.8` |
| [@tanstack/react-router-ssr-query](https://github.com/TanStack/router/tree/HEAD/packages/react-router-ssr-query) | `1.161.3` | `1.162.8` |
| [@tanstack/react-start](https://github.com/TanStack/router/tree/HEAD/packages/react-start) | `1.161.3` | `1.162.8` |
| [better-auth](https://github.com/better-auth/better-auth/tree/HEAD/packages/better-auth) | `1.4.18` | `1.4.19` |
| [hono](https://github.com/honojs/hono) | `4.11.9` | `4.12.2` |
| [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `0.574.0` | `0.575.0` |
| [react-hook-form](https://github.com/react-hook-form/react-hook-form) | `7.71.1` | `7.71.2` |
| [tailwind-merge](https://github.com/dcastil/tailwind-merge) | `3.4.1` | `3.5.0` |
| [@happy-dom/global-registrator](https://github.com/capricorn86/happy-dom) | `20.6.1` | `20.7.0` |
| [@tailwindcss/vite](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-vite) | `4.1.18` | `4.2.1` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.2.3` | `25.3.0` |
| [oxfmt](https://github.com/oxc-project/oxc/tree/HEAD/npm/oxfmt) | `0.33.0` | `0.35.0` |
| [oxlint](https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint) | `1.48.0` | `1.50.0` |
| [oxlint-tsgolint](https://github.com/oxc-project/tsgolint) | `0.14.0` | `0.14.2` |
| [tailwindcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss) | `4.1.18` | `4.2.1` |



Updates `@inquirer/prompts` from 8.2.1 to 8.3.0
- [Release notes](https://github.com/SBoudrias/Inquirer.js/releases)
- [Commits](https://github.com/SBoudrias/Inquirer.js/compare/@inquirer/prompts@8.2.1...@inquirer/prompts@8.3.0)

Updates `@scalar/hono-api-reference` from 0.9.41 to 0.9.44
- [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.1.0 to 0.3.0
- [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.0/packages/react-hotkeys)

Updates `@tanstack/react-router` from 1.161.3 to 1.162.8
- [Release notes](https://github.com/TanStack/router/releases)
- [Commits](https://github.com/TanStack/router/commits/v1.162.8/packages/react-router)

Updates `@tanstack/react-router-ssr-query` from 1.161.3 to 1.162.8
- [Release notes](https://github.com/TanStack/router/releases)
- [Commits](https://github.com/TanStack/router/commits/v1.162.8/packages/react-router-ssr-query)

Updates `@tanstack/react-start` from 1.161.3 to 1.162.8
- [Release notes](https://github.com/TanStack/router/releases)
- [Commits](https://github.com/TanStack/router/commits/v1.162.8/packages/react-start)

Updates `better-auth` from 1.4.18 to 1.4.19
- [Release notes](https://github.com/better-auth/better-auth/releases)
- [Commits](https://github.com/better-auth/better-auth/commits/v1.4.19/packages/better-auth)

Updates `hono` from 4.11.9 to 4.12.2
- [Release notes](https://github.com/honojs/hono/releases)
- [Commits](https://github.com/honojs/hono/compare/v4.11.9...v4.12.2)

Updates `lucide-react` from 0.574.0 to 0.575.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/0.575.0/packages/lucide-react)

Updates `react-hook-form` from 7.71.1 to 7.71.2
- [Release notes](https://github.com/react-hook-form/react-hook-form/releases)
- [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md)
- [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.71.1...v7.71.2)

Updates `tailwind-merge` from 3.4.1 to 3.5.0
- [Release notes](https://github.com/dcastil/tailwind-merge/releases)
- [Commits](https://github.com/dcastil/tailwind-merge/compare/v3.4.1...v3.5.0)

Updates `@happy-dom/global-registrator` from 20.6.1 to 20.7.0
- [Release notes](https://github.com/capricorn86/happy-dom/releases)
- [Commits](https://github.com/capricorn86/happy-dom/compare/v20.6.1...v20.7.0)

Updates `@tailwindcss/vite` from 4.1.18 to 4.2.1
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.2.1/packages/@tailwindcss-vite)

Updates `@types/node` from 25.2.3 to 25.3.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `oxfmt` from 0.33.0 to 0.35.0
- [Release notes](https://github.com/oxc-project/oxc/releases)
- [Changelog](https://github.com/oxc-project/oxc/blob/main/npm/oxfmt/CHANGELOG.md)
- [Commits](https://github.com/oxc-project/oxc/commits/oxfmt_v0.35.0/npm/oxfmt)

Updates `oxlint` from 1.48.0 to 1.50.0
- [Release notes](https://github.com/oxc-project/oxc/releases)
- [Changelog](https://github.com/oxc-project/oxc/blob/main/npm/oxlint/CHANGELOG.md)
- [Commits](https://github.com/oxc-project/oxc/commits/oxlint_v1.50.0/npm/oxlint)

Updates `oxlint-tsgolint` from 0.14.0 to 0.14.2
- [Release notes](https://github.com/oxc-project/tsgolint/releases)
- [Commits](https://github.com/oxc-project/tsgolint/compare/v0.14.0...v0.14.2)

Updates `tailwindcss` from 4.1.18 to 4.2.1
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.2.1/packages/tailwindcss)

---
updated-dependencies:
- dependency-name: "@inquirer/prompts"
  dependency-version: 8.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: "@scalar/hono-api-reference"
  dependency-version: 0.9.44
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@tanstack/react-hotkeys"
  dependency-version: 0.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: "@tanstack/react-router"
  dependency-version: 1.162.8
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: "@tanstack/react-router-ssr-query"
  dependency-version: 1.162.8
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: "@tanstack/react-start"
  dependency-version: 1.162.8
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: better-auth
  dependency-version: 1.4.19
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: hono
  dependency-version: 4.12.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: lucide-react
  dependency-version: 0.575.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: react-hook-form
  dependency-version: 7.71.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: tailwind-merge
  dependency-version: 3.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: "@happy-dom/global-registrator"
  dependency-version: 20.7.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: "@tailwindcss/vite"
  dependency-version: 4.2.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: "@types/node"
  dependency-version: 25.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: oxfmt
  dependency-version: 0.35.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: oxlint
  dependency-version: 1.50.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: oxlint-tsgolint
  dependency-version: 0.14.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: tailwindcss
  dependency-version: 4.2.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-24 19:05:53 +01:00
Nico
74b21d93d6
fix: add DisableTLS param if server is http (#563)
Closes #561
2026-02-23 21:31:53 +01:00
Nico
a0a813ed09
refactor: short id branded type (#552)
* refactor: short id branded type

* chore: pr feedbacks
2026-02-21 11:16:15 +01:00
Nico
45fd9f9de1
feat: repository used space (#551)
* feat: repository used space

refactor: use a smarter cache key logic for easier invalidations

* chore: pr feedbacks
2026-02-21 10:52:55 +01:00
Nico
182d39a887
feat: restore snapshot as tar (#550)
* feat: export snapshot as tar file

chore(mutext): prevent double lock release

* chore: pr feedbacks

* fix: dump single file no tar

* chore: pr feedbacks
2026-02-21 10:19:20 +01:00
Nico
8681ebc0c0
refactor: always use short id in api calls (#545) 2026-02-19 20:08:40 +01:00
Nicolas Meienberger
f3608b0f30 docs: update TROUBLESHOOTING.md with LOG_LEVEL instruction 2026-02-18 21:55:46 +01:00
Nico
8fcd446926
refactor: snapshot strip out base path (#542)
* refactor: strip out volume path in snapshot list / restore

chore: lint issue

* test: backups new include patterns
2026-02-18 21:45:19 +01:00
Nico
ca8248b2a0
ui: scroll to error (#541)
* refactor: scroll to first error when submitting a form

* refactor: split file browsers into dedicated components with base

* chore: pr feedbacks
2026-02-18 20:13:09 +01:00
Nicolas Meienberger
505f60a8a1 docs: update readme version 2026-02-17 20:58:32 +01:00
Nicolas Meienberger
705f5c75cb chore: remove useless migration normalization
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2026-02-17 20:50:23 +01:00
Nicolas Meienberger
1321b15c0d chore: revert drizzle-orm to previous version
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2026-02-17 20:33:24 +01:00
Nicolas Meienberger
289832d58b e2e: fix flaky tests by adding page readiness chaeck
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2026-02-17 20:28:18 +01:00
Nicolas Meienberger
85e2272a5c fix(ui): missing @container class in backup details 2026-02-17 19:29:10 +01:00
Nicolas Meienberger
c04219cbf7 fix: normalize drizzle migration timestamp
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2026-02-17 19:05:52 +01:00
Nicolas Meienberger
d3be8e7a42 chore: add back e2e tests in release workflow 2026-02-17 18:48:04 +01:00
Nico
f059a23ecc
fix: multiple mobile and responsiveness issues (#537)
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
* fix: multiple mobile and responsiveness issues

fix(mobile): scroll reset on snapshot selection

fix(mobile): layout improvements

refactor(volume-details): improve layout

refactor: better card breakpoints layouts in backps list

fix(ui): keep sidebar in the state it was before reloading

refactor(ui): keep the same grid size in all breakpoints

refactor: manual hotkey devpanel to tanstack hotkeys

* chore: pr feedback
2026-02-17 18:44:22 +01:00
dependabot[bot]
16df0dc875
chore(deps): bump the minor-patch group across 1 directory with 11 updates (#535)
Bumps the minor-patch group with 11 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@inquirer/prompts](https://github.com/SBoudrias/Inquirer.js) | `8.2.0` | `8.2.1` |
| [@scalar/hono-api-reference](https://github.com/scalar/scalar/tree/HEAD/integrations/hono) | `0.9.40` | `0.9.41` |
| [@tanstack/react-router](https://github.com/TanStack/router/tree/HEAD/packages/react-router) | `1.159.10` | `1.160.2` |
| [@tanstack/react-router-devtools](https://github.com/TanStack/router/tree/HEAD/packages/react-router-devtools) | `1.159.10` | `1.160.2` |
| [@tanstack/react-router-ssr-query](https://github.com/TanStack/router/tree/HEAD/packages/react-router-ssr-query) | `1.159.10` | `1.160.2` |
| [@tanstack/react-start](https://github.com/TanStack/router/tree/HEAD/packages/react-start) | `1.159.12` | `1.160.2` |
| [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `0.564.0` | `0.574.0` |
| [tailwind-merge](https://github.com/dcastil/tailwind-merge) | `3.4.0` | `3.4.1` |
| [oxfmt](https://github.com/oxc-project/oxc/tree/HEAD/npm/oxfmt) | `0.32.0` | `0.33.0` |
| [oxlint](https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint) | `1.47.0` | `1.48.0` |
| [oxlint-tsgolint](https://github.com/oxc-project/tsgolint) | `0.12.2` | `0.14.0` |



Updates `@inquirer/prompts` from 8.2.0 to 8.2.1
- [Release notes](https://github.com/SBoudrias/Inquirer.js/releases)
- [Commits](https://github.com/SBoudrias/Inquirer.js/compare/@inquirer/prompts@8.2.0...@inquirer/prompts@8.2.1)

Updates `@scalar/hono-api-reference` from 0.9.40 to 0.9.41
- [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-router` from 1.159.10 to 1.160.2
- [Release notes](https://github.com/TanStack/router/releases)
- [Commits](https://github.com/TanStack/router/commits/v1.160.2/packages/react-router)

Updates `@tanstack/react-router-devtools` from 1.159.10 to 1.160.2
- [Release notes](https://github.com/TanStack/router/releases)
- [Commits](https://github.com/TanStack/router/commits/v1.160.2/packages/react-router-devtools)

Updates `@tanstack/react-router-ssr-query` from 1.159.10 to 1.160.2
- [Release notes](https://github.com/TanStack/router/releases)
- [Commits](https://github.com/TanStack/router/commits/v1.160.2/packages/react-router-ssr-query)

Updates `@tanstack/react-start` from 1.159.12 to 1.160.2
- [Release notes](https://github.com/TanStack/router/releases)
- [Commits](https://github.com/TanStack/router/commits/v1.160.2/packages/react-start)

Updates `lucide-react` from 0.564.0 to 0.574.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/0.574.0/packages/lucide-react)

Updates `tailwind-merge` from 3.4.0 to 3.4.1
- [Release notes](https://github.com/dcastil/tailwind-merge/releases)
- [Commits](https://github.com/dcastil/tailwind-merge/compare/v3.4.0...v3.4.1)

Updates `oxfmt` from 0.32.0 to 0.33.0
- [Release notes](https://github.com/oxc-project/oxc/releases)
- [Changelog](https://github.com/oxc-project/oxc/blob/main/npm/oxfmt/CHANGELOG.md)
- [Commits](https://github.com/oxc-project/oxc/commits/oxfmt_v0.33.0/npm/oxfmt)

Updates `oxlint` from 1.47.0 to 1.48.0
- [Release notes](https://github.com/oxc-project/oxc/releases)
- [Changelog](https://github.com/oxc-project/oxc/blob/main/npm/oxlint/CHANGELOG.md)
- [Commits](https://github.com/oxc-project/oxc/commits/oxlint_v1.48.0/npm/oxlint)

Updates `oxlint-tsgolint` from 0.12.2 to 0.14.0
- [Release notes](https://github.com/oxc-project/tsgolint/releases)
- [Commits](https://github.com/oxc-project/tsgolint/compare/v0.12.2...v0.14.0)

---
updated-dependencies:
- dependency-name: "@inquirer/prompts"
  dependency-version: 8.2.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@scalar/hono-api-reference"
  dependency-version: 0.9.41
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@tanstack/react-router"
  dependency-version: 1.160.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: "@tanstack/react-router-devtools"
  dependency-version: 1.160.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.160.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: "@tanstack/react-start"
  dependency-version: 1.160.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: lucide-react
  dependency-version: 0.574.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: tailwind-merge
  dependency-version: 3.4.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: oxfmt
  dependency-version: 0.33.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: oxlint
  dependency-version: 1.48.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: oxlint-tsgolint
  dependency-version: 0.14.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-17 18:33:11 +01:00
Nico
ebfafa4143
fix(repositories): append short id when using the default path for local repos (#536) 2026-02-17 18:28:33 +01:00
Nicolas Meienberger
0bbdbc85ad Reapply "chore(deps): bump drizzle-orm from 1.0.0-beta.9-e89174b to 1.0.0-beta.15-859cf75 (#501)"
This reverts commit db163a1bb5.
2026-02-17 17:52:25 +01:00
Nicolas Meienberger
440916e312 ci: skip e2e temporarily
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2026-02-16 23:59:08 +01:00
Nicolas Meienberger
d58329d86e fix: skip migrating imported repos
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2026-02-16 23:50:39 +01:00
Nicolas Meienberger
a70e95fb7e chore: enable foreign keys constraints in CLI runs
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2026-02-16 23:29:36 +01:00
Nicolas Meienberger
7bcf380198 test: fix impossible test scenarios
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2026-02-16 23:20:33 +01:00
Nicolas Meienberger
11efe87b2e fix: apply fk db constraint on app creation 2026-02-16 23:09:18 +01:00
Nicolas Meienberger
db163a1bb5 Revert "chore(deps): bump drizzle-orm from 1.0.0-beta.9-e89174b to 1.0.0-beta.15-859cf75 (#501)"
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
This reverts commit d53b24bfb4.
2026-02-16 22:18:10 +01:00
Nicolas Meienberger
6733151bca refactor: only set pragma on server start
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2026-02-16 21:57:14 +01:00
Nicolas Meienberger
bf82bbf48a test(e2e): ensure cascade delete works
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2026-02-16 21:41:52 +01:00
Nico
dda7b9939f
refactor: allow more characters in usernames (#529)
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2026-02-16 21:37:15 +01:00
Nico
99bb296866
fix: cascade delete not correctly applied (#531) 2026-02-16 21:36:07 +01:00
Nico
de1278a416
refactor: rename DATABASE_URL -> ZEROBYTE_DATABASE_URL (#528) 2026-02-16 19:43:43 +01:00
Nico
66ed89d39e
fix(file-tree): display load more if root folder has 500+ items (#526)
Close #518
2026-02-16 19:25:41 +01:00
dependabot[bot]
fdd8edec70
chore(deps): bump the minor-patch group across 1 directory with 11 updates (#514)
Bumps the minor-patch group with 11 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@tanstack/react-router](https://github.com/TanStack/router/tree/HEAD/packages/react-router) | `1.159.5` | `1.159.6` |
| [@tanstack/react-router-devtools](https://github.com/TanStack/router/tree/HEAD/packages/react-router-devtools) | `1.159.5` | `1.159.6` |
| [@tanstack/react-router-ssr-query](https://github.com/TanStack/router/tree/HEAD/packages/react-router-ssr-query) | `1.159.5` | `1.159.6` |
| [@tanstack/react-start](https://github.com/TanStack/router/tree/HEAD/packages/react-start) | `1.159.5` | `1.159.7` |
| [dotenv](https://github.com/motdotla/dotenv) | `17.2.4` | `17.3.1` |
| [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `0.563.0` | `0.564.0` |
| [@hey-api/openapi-ts](https://github.com/hey-api/openapi-ts) | `0.92.3` | `0.92.4` |
| [oxfmt](https://github.com/oxc-project/oxc/tree/HEAD/npm/oxfmt) | `0.31.0` | `0.32.0` |
| [oxlint](https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint) | `1.46.0` | `1.47.0` |
| [oxlint-tsgolint](https://github.com/oxc-project/tsgolint) | `0.12.0` | `0.12.2` |
| [vite-tsconfig-paths](https://github.com/aleclarson/vite-tsconfig-paths) | `6.1.0` | `6.1.1` |



Updates `@tanstack/react-router` from 1.159.5 to 1.159.6
- [Release notes](https://github.com/TanStack/router/releases)
- [Commits](https://github.com/TanStack/router/commits/v1.159.6/packages/react-router)

Updates `@tanstack/react-router-devtools` from 1.159.5 to 1.159.6
- [Release notes](https://github.com/TanStack/router/releases)
- [Commits](https://github.com/TanStack/router/commits/v1.159.6/packages/react-router-devtools)

Updates `@tanstack/react-router-ssr-query` from 1.159.5 to 1.159.6
- [Release notes](https://github.com/TanStack/router/releases)
- [Commits](https://github.com/TanStack/router/commits/v1.159.6/packages/react-router-ssr-query)

Updates `@tanstack/react-start` from 1.159.5 to 1.159.7
- [Release notes](https://github.com/TanStack/router/releases)
- [Commits](https://github.com/TanStack/router/commits/v1.159.7/packages/react-start)

Updates `dotenv` from 17.2.4 to 17.3.1
- [Changelog](https://github.com/motdotla/dotenv/blob/master/CHANGELOG.md)
- [Commits](https://github.com/motdotla/dotenv/compare/v17.2.4...v17.3.1)

Updates `lucide-react` from 0.563.0 to 0.564.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/0.564.0/packages/lucide-react)

Updates `@hey-api/openapi-ts` from 0.92.3 to 0.92.4
- [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.92.3...@hey-api/openapi-ts@0.92.4)

Updates `oxfmt` from 0.31.0 to 0.32.0
- [Release notes](https://github.com/oxc-project/oxc/releases)
- [Changelog](https://github.com/oxc-project/oxc/blob/main/npm/oxfmt/CHANGELOG.md)
- [Commits](https://github.com/oxc-project/oxc/commits/oxfmt_v0.32.0/npm/oxfmt)

Updates `oxlint` from 1.46.0 to 1.47.0
- [Release notes](https://github.com/oxc-project/oxc/releases)
- [Changelog](https://github.com/oxc-project/oxc/blob/main/npm/oxlint/CHANGELOG.md)
- [Commits](https://github.com/oxc-project/oxc/commits/oxlint_v1.47.0/npm/oxlint)

Updates `oxlint-tsgolint` from 0.12.0 to 0.12.2
- [Release notes](https://github.com/oxc-project/tsgolint/releases)
- [Commits](https://github.com/oxc-project/tsgolint/compare/v0.12.0...v0.12.2)

Updates `vite-tsconfig-paths` from 6.1.0 to 6.1.1
- [Release notes](https://github.com/aleclarson/vite-tsconfig-paths/releases)
- [Commits](https://github.com/aleclarson/vite-tsconfig-paths/compare/v6.1.0...v6.1.1)

---
updated-dependencies:
- dependency-name: "@tanstack/react-router"
  dependency-version: 1.159.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@tanstack/react-router-devtools"
  dependency-version: 1.159.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@tanstack/react-router-ssr-query"
  dependency-version: 1.159.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@tanstack/react-start"
  dependency-version: 1.159.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: dotenv
  dependency-version: 17.3.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: lucide-react
  dependency-version: 0.564.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: "@hey-api/openapi-ts"
  dependency-version: 0.92.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: oxfmt
  dependency-version: 0.32.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: oxlint
  dependency-version: 1.47.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: oxlint-tsgolint
  dependency-version: 0.12.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: vite-tsconfig-paths
  dependency-version: 6.1.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-14 14:42:45 +01:00
Nico
39ae0cfe6d
fix: remove trailing slash or space before constructing s3 uri (#516)
Closes #496
2026-02-14 14:18:36 +01:00
dependabot[bot]
d53b24bfb4
chore(deps): bump drizzle-orm from 1.0.0-beta.9-e89174b to 1.0.0-beta.15-859cf75 (#501)
* chore(deps): bump drizzle-orm

Bumps [drizzle-orm](https://github.com/drizzle-team/drizzle-orm) from 1.0.0-beta.9-e89174b to 1.0.0-beta.15-859cf75.
- [Release notes](https://github.com/drizzle-team/drizzle-orm/releases)
- [Commits](https://github.com/drizzle-team/drizzle-orm/commits)

---
updated-dependencies:
- dependency-name: drizzle-orm
  dependency-version: 1.0.0-beta.15-859cf75
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* refactor: convert async transactions to sync + .run()

* chore: formatting

* chore: add lefthook dep

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Nicolas Meienberger <github@thisprops.com>
2026-02-14 14:18:20 +01:00
dependabot[bot]
ed250ba03d
chore(deps-dev): bump drizzle-kit (#502)
Bumps [drizzle-kit](https://github.com/drizzle-team/drizzle-orm) from 1.0.0-beta.9-e89174b to 1.0.0-beta.15-859cf75.
- [Release notes](https://github.com/drizzle-team/drizzle-orm/releases)
- [Commits](https://github.com/drizzle-team/drizzle-orm/commits)

---
updated-dependencies:
- dependency-name: drizzle-kit
  dependency-version: 1.0.0-beta.15-859cf75
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-14 13:55:31 +01:00
Nico
8e5eb68935
refactor: optimistic ui when deleting a snapshot (#515) 2026-02-14 13:47:09 +01:00
Nico
bad944a232
feat: restore progress (#281)
* feat: restore progress

* feat: keep restore progress on reload

* refactor: centralize sse event types

* refactor(sse): generic handler factory
2026-02-14 12:35:16 +01:00
Nico
1017f1a38b
feat: edit repository form (#507)
* feat: edit repository form

* refactor: local repo path concat as a code migration

* refactor: server constants

* chore: fix lint issue in test file

* refactor: add auth to getServerConstants
2026-02-14 11:49:33 +01:00
Nicolas Meienberger
60c8bd77f5 chore: bump shoutrrr to v0.13.2
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2026-02-13 22:07:10 +01:00
Nicolas Meienberger
38c1cf4373 ci: don't block build on anchore alerts 2026-02-13 22:01:18 +01:00
Nico
a4fbe3c8df
remove proxy pattern in db and auth (#513)
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
* refactor: add nitro bootstrap plugin to ensure app is started before first call

* refactor(bootstrap): avoid duplicate event firing

* refactor: extract common setup in initModule function

* refactor: remove proxy pattern for db and auth

Since we migrated away from rr this is not needed anymore as the bundler
correctly split chunks
2026-02-13 21:18:40 +01:00
Nico
297e14ebb2
refactor: add nitro bootstrap plugin to ensure app is started before first call (#512)
* refactor: add nitro bootstrap plugin to ensure app is started before first call

* refactor(bootstrap): avoid duplicate event firing
2026-02-13 20:46:10 +01:00
Nicolas Meienberger
1fea2a729a e2e: fix tests
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2026-02-13 19:12:41 +01:00
Nicolas
613f380ea7 chore: downgrade bun to v1.3.6 again due to user crashes 2026-02-13 18:40:16 +01:00
Nicolas
0cf5358f77 ci: fix linting issues
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2026-02-13 18:33:34 +01:00
Nicolas
6d33a3a35e refactor: remove delete option until we have a proper dry-run mode 2026-02-13 18:31:06 +01:00
Nicolas
71f72bd09b fix: use 127.0.0.1 when making server to server requests 2026-02-13 18:25:03 +01:00
Nico
ec4cf938bc
feat: show progress indicator on mirrors (#499)
* feat: show progress indicator on mirrors

refactor: remove unnecessary useEffects

* refactor: pr feedbacks
2026-02-12 22:27:30 +01:00
Nico
7ebce1166b
feat: expand snapshot details with additional info (#505)
* feat: extend snapshot details with more info

Closes #385

* refactor: centralize restic backup schemas

* refactor: pr feedbacks
2026-02-12 18:25:21 +01:00
dependabot[bot]
07aa2bf768
chore(deps): bump the minor-patch group with 21 updates (#500) 2026-02-12 07:55:17 +01:00
Nico
825d46c934
refactor: react-router -> tanstack start (#498)
* refactor: move to tanstack start

* refactor: auth flow & volumes

* refactor: repo & snapshot details

* refactor: backups, create repo, volumes

* refactor: create volume & restore snapshot

* refactor: notifications

* refactor: settings

* refactor: breadcrumbs

* fix: ts issues

* refactor: prod deployment

* fix: import css production

* refactor: nitro build

* refactor: winston -> consola

* fix: memory leak is sse events cleanup

* fix: cli usage

* chore: remove rr routes file

* refactor: pr feedbacks

* refactor: patch api client to have a global client per call

* refactor: pr feedbacks

* fix(dockerfile): add explicit port

* fix(e2e): healthcheck under /api
2026-02-11 21:41:06 +01:00
Nico
b45d36e06a
refactor: make lock errors cleaner and show unlock button (#493)
* refactor: make lock errors cleaner and show unlock button

* chore: pr feedbacks
2026-02-10 20:18:25 +01:00
Nico
cb7988b8ed
refactor(retention badges): use restic forget dry run result instead of manual calc (#494)
* refactor(retention badges): use restic forget dry run result instead of manual calc

* chore: PR feedbacks
2026-02-10 20:14:35 +01:00
Nico
12d0eda6ef
feat: dev panel (#489)
* feat: dev panel

* chore: fix typing issue
2026-02-09 22:04:21 +01:00
Nico
413e86b8b9
ci: create a nightly build pipeline (#487) 2026-02-09 18:21:37 +01:00
Nico
124b8f4081
fix: make retention badge clickable for mobile devices (#486) 2026-02-09 18:15:34 +01:00
Nicolas Meienberger
81d3a76518 chore: bump Bun to 1.3.9
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2026-02-08 16:44:20 +01:00
Nicolas Meienberger
2d0ae89727 fix: get volume if short id is only numbers
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2026-02-08 16:33:32 +01:00
Nico
09c1cbbb94
feat: show retention tags in snapshots timeline (#462)
* feat: show retention tags in snapshots timeline

* refactor: use button for accesibility
2026-02-08 16:14:43 +01:00
Nico
0e0eaea946
refactor: split backups module + unit tests (#463)
* refactor: split backups module

* test: backup execution

* test: schedule db
2026-02-03 23:00:25 +01:00
Nicolas Meienberger
cb9186adc0 docs(troubleshooting): add section about rclon sftp
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2026-02-02 21:27:26 +01:00
Nicolas Meienberger
1e5921fa61 fix(create-repo): directory selection 2026-02-02 20:34:37 +01:00
Nicolas Meienberger
cc081ca64c fix: downgrade bun to 1.3.6
Closes #457 (related to https://github.com/oven-sh/bun/issues/26544)
2026-02-02 18:20:50 +01:00
Nico
17b4027e0b
refactor: split create schedule form (#453) 2026-02-02 18:18:14 +01:00
Nico
35773a6969
refactor: upgrade to drizzle v1 (#450)
* refactor: move migrations to new structure

* refactor: convert all findMany to new structure

* fix(backups-schedule): missing null matching for last backup status

* chore: move root lib to server
2026-02-01 19:14:52 +01:00
Nicolas Meienberger
ebdca8870b docs(troubleshooting): fix anchors 2026-02-01 12:01:58 +01:00
Nicolas Meienberger
4563cd3a99 docs: improve troubleshooting guide for rclone 2026-02-01 11:58:54 +01:00
Nicolas Meienberger
37861089ba chore: fix docker-compose dev
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2026-02-01 10:32:59 +01:00
Nico
b914851841
fix: smb mount issues (#442) 2026-02-01 10:27:26 +01:00
Nico
4c7bd31b88
feat: custom rclone conf dir env variable (#432)
* feat: custom rclone conf dir env variable

Closes #238

* chore: rename RCLONE_CONF_DIR -> RCLONE_CONFIG_DIR
2026-02-01 10:24:09 +01:00
Nicolas Meienberger
1a65cee738 chore: bump rclone to 1.73.0 2026-02-01 10:22:32 +01:00
Nicolas Meienberger
392b2f78af chore: enable react compiler 2026-01-31 22:45:11 +01:00
Nico
5bb5fcd09c
refactor: paginate large file counts (#441)
* refactor: add pagination to handle volume folders with extremely large folder counts

* refactor: stream restic ls result

* test: file-tree load more

* refactor: string params

* fix(tsc): string pagination params

* chore: pr feedbacks
2026-01-31 16:05:42 +01:00
Nico
b8ad1ae41a
refactor: show progress card even if no data is streamed (#440) 2026-01-31 13:26:10 +01:00
Nicolas Meienberger
d4864099a6 style(create-backup): fix cards overlap 2026-01-31 10:44:51 +01:00
Nicolas Meienberger
6f8ff9d606 refactor: add body limit middleware 2026-01-30 21:36:15 +01:00
Nicolas Meienberger
e70609e116 e2e: fix select value with new non-slugified name
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
2026-01-30 18:24:40 +01:00
Nicolas Meienberger
774c11a9ac revert(sftp-repo): put back previous values for alive count and alive interval 2026-01-30 18:16:07 +01:00
Nicolas Meienberger
1118f45788 chore: update readme with mandatory env variables 2026-01-30 18:01:41 +01:00
Nico
8ced05c5a9
feat: add refresh snapshot list and snapshot error page (#434)
Closes #372
2026-01-29 23:20:13 +01:00
dependabot[bot]
a3ba6e093c
chore(deps): bump the minor-patch group with 3 updates (#433)
Bumps the minor-patch group with 3 updates: [better-auth](https://github.com/better-auth/better-auth/tree/HEAD/packages/better-auth), [@hey-api/openapi-ts](https://github.com/hey-api/openapi-ts) and [@types/bun](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/bun).


Updates `better-auth` from 1.4.17 to 1.4.18
- [Release notes](https://github.com/better-auth/better-auth/releases)
- [Commits](https://github.com/better-auth/better-auth/commits/v1.4.18/packages/better-auth)

Updates `@hey-api/openapi-ts` from 0.90.10 to 0.91.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.90.10...@hey-api/openapi-ts@0.91.0)

Updates `@types/bun` from 1.3.7 to 1.3.8
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/bun)

---
updated-dependencies:
- dependency-name: better-auth
  dependency-version: 1.4.18
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@hey-api/openapi-ts"
  dependency-version: 0.91.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: "@types/bun"
  dependency-version: 1.3.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-29 23:19:44 +01:00
Nico
da5ad5c817
feat: add config for SMB guest mode (#431)
* feat: add config for SMB guest mode

Closes #279

* chore: pr feedbacks
2026-01-29 23:16:31 +01:00
Nicolas Meienberger
f62ae222ca chore: bump bun to 1.3.8 2026-01-29 22:56:33 +01:00
Nico
da3f88f37d
Update README with TrueNAS configuration guidance
Add note for TrueNAS users regarding ephemeral host path.
2026-01-29 22:18:29 +01:00
dependabot[bot]
1f2225d326
chore(deps): bump the minor-patch group across 1 directory with 8 updates (#427)
Bumps the minor-patch group with 8 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@scalar/hono-api-reference](https://github.com/scalar/scalar/tree/HEAD/integrations/hono) | `0.9.37` | `0.9.38` |
| [hono](https://github.com/honojs/hono) | `4.11.6` | `4.11.7` |
| [react-router-hono-server](https://github.com/rphlmr/react-router-hono-server) | `2.23.0` | `2.24.0` |
| [@happy-dom/global-registrator](https://github.com/capricorn86/happy-dom) | `20.3.9` | `20.4.0` |
| [@types/bun](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/bun) | `1.3.6` | `1.3.7` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.0.10` | `25.1.0` |
| [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) | `19.2.9` | `19.2.10` |
| [oxlint-tsgolint](https://github.com/oxc-project/tsgolint) | `0.11.2` | `0.11.3` |



Updates `@scalar/hono-api-reference` from 0.9.37 to 0.9.38
- [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 `hono` from 4.11.6 to 4.11.7
- [Release notes](https://github.com/honojs/hono/releases)
- [Commits](https://github.com/honojs/hono/compare/v4.11.6...v4.11.7)

Updates `react-router-hono-server` from 2.23.0 to 2.24.0
- [Release notes](https://github.com/rphlmr/react-router-hono-server/releases)
- [Changelog](https://github.com/rphlmr/react-router-hono-server/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rphlmr/react-router-hono-server/compare/v2.23.0...v2.24.0)

Updates `@happy-dom/global-registrator` from 20.3.9 to 20.4.0
- [Release notes](https://github.com/capricorn86/happy-dom/releases)
- [Commits](https://github.com/capricorn86/happy-dom/compare/v20.3.9...v20.4.0)

Updates `@types/bun` from 1.3.6 to 1.3.7
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/bun)

Updates `@types/node` from 25.0.10 to 25.1.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `@types/react` from 19.2.9 to 19.2.10
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

Updates `oxlint-tsgolint` from 0.11.2 to 0.11.3
- [Release notes](https://github.com/oxc-project/tsgolint/releases)
- [Commits](https://github.com/oxc-project/tsgolint/compare/v0.11.2...v0.11.3)

---
updated-dependencies:
- dependency-name: "@scalar/hono-api-reference"
  dependency-version: 0.9.38
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: hono
  dependency-version: 4.11.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: react-router-hono-server
  dependency-version: 2.24.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: "@happy-dom/global-registrator"
  dependency-version: 20.4.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: "@types/bun"
  dependency-version: 1.3.7
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@types/node"
  dependency-version: 25.1.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: "@types/react"
  dependency-version: 19.2.10
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: oxlint-tsgolint
  dependency-version: 0.11.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-29 21:14:40 +01:00
Nico
3786a9283b
fix(backup-card): dot invisible if volume name too long (#430) 2026-01-29 21:14:21 +01:00
Nico
bde5302ca4
feat(smtp-notification): add missing from name param to form (#429)
* feat(smtp-notification): add missing from name param to form

* chore: gen api client
2026-01-29 21:13:51 +01:00
Nico
93aabebd51
refactor: remove slug constraint in volume name (#428)
* refactor: remove slug constraint in volume name

Closes #413

* chore: remove un-used imports

* fix(notifications): validate name is not empty

* refactor: deny empty names
2026-01-29 20:57:20 +01:00
Nicolas Meienberger
861f81f14e refactor: use bun random uuid v7 for member id
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2026-01-29 08:57:46 +01:00
Nicolas Meienberger
e8318d577b fix: user missing their org on startup
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2026-01-29 08:31:58 +01:00
Nicolas Meienberger
dfb4890c44 fix: remove new migration dependency on legacy re-key one
Some checks failed
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2026-01-29 07:51:02 +01:00
Nicolas Meienberger
7d131719b7 fix: skip read data for doctor operation
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2026-01-29 07:38:51 +01:00
Nicolas Meienberger
726bd5d06f chore: skip secure headers in non-production envrionments 2026-01-28 22:15:42 +01:00
Nicolas Meienberger
a0c959a375 chore: fix e2e
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2026-01-28 01:34:21 +01:00
Nicolas Meienberger
e5bd97ed2c chore: update readme 2026-01-28 01:27:27 +01:00
Nico
45032362a8
feat: base-url env variable (#419) 2026-01-28 01:19:37 +01:00
Nicolas Meienberger
8f8b370679 test: fix cookie name
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2026-01-27 23:25:28 +01:00
Nicolas Meienberger
41d7cf8b94 revert: dependencies downgrade 2026-01-27 23:06:39 +01:00
Nicolas Meienberger
4011ed778f chore: rename cookie prefix to zerobyte 2026-01-27 23:02:34 +01:00
Nicolas Meienberger
0ec23da8ab fix: use custom cookie prefix
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2026-01-27 22:48:54 +01:00
Nicolas Meienberger
571489a10b chore: downgrade dependencies
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2026-01-27 22:29:20 +01:00
Nicolas Meienberger
da329826c1 fix: add organization for users that don't have one
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2026-01-27 22:03:48 +01:00
Nicolas Meienberger
1fbd6aa3db chore(better-auth): disable origin check if no trusted origins are passed
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2026-01-27 20:21:22 +01:00
Didin Sino
d449547336
feat: add support for sending notifications to Telegram group topics (#401) 2026-01-27 19:43:05 +01:00
dependabot[bot]
eaaaa55c90
chore(deps): bump the minor-patch group with 11 updates (#416)
* chore(deps): bump the minor-patch group with 11 updates

Bumps the minor-patch group with 11 updates:

| Package | From | To |
| --- | --- | --- |
| [@scalar/hono-api-reference](https://github.com/scalar/scalar/tree/HEAD/integrations/hono) | `0.9.35` | `0.9.37` |
| [hono](https://github.com/honojs/hono) | `4.11.5` | `4.11.6` |
| [hono-openapi](https://github.com/rhinobase/hono-openapi) | `1.1.2` | `1.2.0` |
| [isbot](https://github.com/omrilotan/isbot) | `5.1.33` | `5.1.34` |
| [react](https://github.com/facebook/react/tree/HEAD/packages/react) | `19.2.3` | `19.2.4` |
| [react-dom](https://github.com/facebook/react/tree/HEAD/packages/react-dom) | `19.2.3` | `19.2.4` |
| [@happy-dom/global-registrator](https://github.com/capricorn86/happy-dom) | `20.3.7` | `20.3.9` |
| [@hey-api/openapi-ts](https://github.com/hey-api/openapi-ts) | `0.90.9` | `0.90.10` |
| [oxfmt](https://github.com/oxc-project/oxc/tree/HEAD/npm/oxfmt) | `0.26.0` | `0.27.0` |
| [oxlint](https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint) | `1.41.0` | `1.42.0` |
| [oxlint-tsgolint](https://github.com/oxc-project/tsgolint) | `0.11.1` | `0.11.2` |


Updates `@scalar/hono-api-reference` from 0.9.35 to 0.9.37
- [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 `hono` from 4.11.5 to 4.11.6
- [Release notes](https://github.com/honojs/hono/releases)
- [Commits](https://github.com/honojs/hono/compare/v4.11.5...v4.11.6)

Updates `hono-openapi` from 1.1.2 to 1.2.0
- [Release notes](https://github.com/rhinobase/hono-openapi/releases)
- [Commits](https://github.com/rhinobase/hono-openapi/commits/v1.2.0)

Updates `isbot` from 5.1.33 to 5.1.34
- [Changelog](https://github.com/omrilotan/isbot/blob/main/CHANGELOG.md)
- [Commits](https://github.com/omrilotan/isbot/compare/v5.1.33...v5.1.34)

Updates `react` from 19.2.3 to 19.2.4
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v19.2.4/packages/react)

Updates `react-dom` from 19.2.3 to 19.2.4
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v19.2.4/packages/react-dom)

Updates `@happy-dom/global-registrator` from 20.3.7 to 20.3.9
- [Release notes](https://github.com/capricorn86/happy-dom/releases)
- [Commits](https://github.com/capricorn86/happy-dom/compare/v20.3.7...v20.3.9)

Updates `@hey-api/openapi-ts` from 0.90.9 to 0.90.10
- [Release notes](https://github.com/hey-api/openapi-ts/releases)
- [Changelog](https://github.com/hey-api/openapi-ts/blob/main/docs/CHANGELOG.md)
- [Commits](https://github.com/hey-api/openapi-ts/compare/@hey-api/openapi-ts@0.90.9...@hey-api/openapi-ts@0.90.10)

Updates `oxfmt` from 0.26.0 to 0.27.0
- [Release notes](https://github.com/oxc-project/oxc/releases)
- [Changelog](https://github.com/oxc-project/oxc/blob/main/npm/oxfmt/CHANGELOG.md)
- [Commits](https://github.com/oxc-project/oxc/commits/oxfmt_v0.27.0/npm/oxfmt)

Updates `oxlint` from 1.41.0 to 1.42.0
- [Release notes](https://github.com/oxc-project/oxc/releases)
- [Changelog](https://github.com/oxc-project/oxc/blob/main/npm/oxlint/CHANGELOG.md)
- [Commits](https://github.com/oxc-project/oxc/commits/oxlint_v1.42.0/npm/oxlint)

Updates `oxlint-tsgolint` from 0.11.1 to 0.11.2
- [Release notes](https://github.com/oxc-project/tsgolint/releases)
- [Commits](https://github.com/oxc-project/tsgolint/compare/v0.11.1...v0.11.2)

---
updated-dependencies:
- dependency-name: "@scalar/hono-api-reference"
  dependency-version: 0.9.37
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: hono
  dependency-version: 4.11.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: hono-openapi
  dependency-version: 1.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: isbot
  dependency-version: 5.1.34
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: react
  dependency-version: 19.2.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: react-dom
  dependency-version: 19.2.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@happy-dom/global-registrator"
  dependency-version: 20.3.9
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@hey-api/openapi-ts"
  dependency-version: 0.90.10
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: oxfmt
  dependency-version: 0.27.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: oxlint
  dependency-version: 1.42.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: oxlint-tsgolint
  dependency-version: 0.11.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore: remove pinned hono override

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Nicolas Meienberger <github@thisprops.com>
2026-01-27 19:42:24 +01:00
kealuya
c69615b3b6
Description: (#393)
Added support for virtual-host style access for Huawei Cloud OBS. This is achieved by introducing the AWS_S3_BUCKET_LOOKUP environment variable and adding the s3.bucket-lookup=dns option to the parameters.

Changes:

Set AWS_S3_BUCKET_LOOKUP environment variable.
Included s3.bucket-lookup=dns in the S3 configuration options to ensure compatibility with Huawei OBS DNS-based bucket addressing.
2026-01-25 11:23:14 +01:00
dependabot[bot]
ba8b599059
chore(deps): bump the minor-patch group across 1 directory with 17 updates (#404)
* chore(deps): bump the minor-patch group across 1 directory with 17 updates

Bumps the minor-patch group with 17 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@react-router/node](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-node) | `7.12.0` | `7.13.0` |
| [@react-router/serve](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-serve) | `7.12.0` | `7.13.0` |
| [@scalar/hono-api-reference](https://github.com/scalar/scalar/tree/HEAD/integrations/hono) | `0.9.34` | `0.9.35` |
| [@tanstack/react-query](https://github.com/TanStack/query/tree/HEAD/packages/react-query) | `5.90.19` | `5.90.20` |
| [better-auth](https://github.com/better-auth/better-auth/tree/HEAD/packages/better-auth) | `1.4.15` | `1.4.17` |
| [hono](https://github.com/honojs/hono) | `4.11.4` | `4.11.5` |
| [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `0.562.0` | `0.563.0` |
| [react-router](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router) | `7.12.0` | `7.13.0` |
| [recharts](https://github.com/recharts/recharts) | `3.6.0` | `3.7.0` |
| [@happy-dom/global-registrator](https://github.com/capricorn86/happy-dom) | `20.3.3` | `20.3.7` |
| [@hey-api/openapi-ts](https://github.com/hey-api/openapi-ts) | `0.90.4` | `0.90.9` |
| [@playwright/test](https://github.com/microsoft/playwright) | `1.57.0` | `1.58.0` |
| [@react-router/dev](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dev) | `7.12.0` | `7.13.0` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.0.9` | `25.0.10` |
| [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) | `19.2.8` | `19.2.9` |
| [lightningcss](https://github.com/parcel-bundler/lightningcss) | `1.30.2` | `1.31.1` |
| [vite-tsconfig-paths](https://github.com/aleclarson/vite-tsconfig-paths) | `6.0.4` | `6.0.5` |



Updates `@react-router/node` from 7.12.0 to 7.13.0
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-node/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/@react-router/node@7.13.0/packages/react-router-node)

Updates `@react-router/serve` from 7.12.0 to 7.13.0
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-serve/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/@react-router/serve@7.13.0/packages/react-router-serve)

Updates `@scalar/hono-api-reference` from 0.9.34 to 0.9.35
- [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-query` from 5.90.19 to 5.90.20
- [Release notes](https://github.com/TanStack/query/releases)
- [Changelog](https://github.com/TanStack/query/blob/main/packages/react-query/CHANGELOG.md)
- [Commits](https://github.com/TanStack/query/commits/@tanstack/react-query@5.90.20/packages/react-query)

Updates `better-auth` from 1.4.15 to 1.4.17
- [Release notes](https://github.com/better-auth/better-auth/releases)
- [Commits](https://github.com/better-auth/better-auth/commits/v1.4.17/packages/better-auth)

Updates `hono` from 4.11.4 to 4.11.5
- [Release notes](https://github.com/honojs/hono/releases)
- [Commits](https://github.com/honojs/hono/compare/v4.11.4...v4.11.5)

Updates `lucide-react` from 0.562.0 to 0.563.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/0.563.0/packages/lucide-react)

Updates `react-router` from 7.12.0 to 7.13.0
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router@7.13.0/packages/react-router)

Updates `recharts` from 3.6.0 to 3.7.0
- [Release notes](https://github.com/recharts/recharts/releases)
- [Changelog](https://github.com/recharts/recharts/blob/main/CHANGELOG.md)
- [Commits](https://github.com/recharts/recharts/compare/v3.6.0...v3.7.0)

Updates `@happy-dom/global-registrator` from 20.3.3 to 20.3.7
- [Release notes](https://github.com/capricorn86/happy-dom/releases)
- [Commits](https://github.com/capricorn86/happy-dom/compare/v20.3.3...v20.3.7)

Updates `@hey-api/openapi-ts` from 0.90.4 to 0.90.9
- [Release notes](https://github.com/hey-api/openapi-ts/releases)
- [Changelog](https://github.com/hey-api/openapi-ts/blob/main/docs/CHANGELOG.md)
- [Commits](https://github.com/hey-api/openapi-ts/compare/@hey-api/openapi-ts@0.90.4...@hey-api/openapi-ts@0.90.9)

Updates `@playwright/test` from 1.57.0 to 1.58.0
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.57.0...v1.58.0)

Updates `@react-router/dev` from 7.12.0 to 7.13.0
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-dev/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/@react-router/dev@7.13.0/packages/react-router-dev)

Updates `@types/node` from 25.0.9 to 25.0.10
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `@types/react` from 19.2.8 to 19.2.9
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

Updates `lightningcss` from 1.30.2 to 1.31.1
- [Release notes](https://github.com/parcel-bundler/lightningcss/releases)
- [Commits](https://github.com/parcel-bundler/lightningcss/commits)

Updates `vite-tsconfig-paths` from 6.0.4 to 6.0.5
- [Release notes](https://github.com/aleclarson/vite-tsconfig-paths/releases)
- [Commits](https://github.com/aleclarson/vite-tsconfig-paths/compare/v6.0.4...v6.0.5)

---
updated-dependencies:
- dependency-name: "@react-router/node"
  dependency-version: 7.13.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: "@react-router/serve"
  dependency-version: 7.13.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: "@scalar/hono-api-reference"
  dependency-version: 0.9.35
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@tanstack/react-query"
  dependency-version: 5.90.20
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: better-auth
  dependency-version: 1.4.17
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: hono
  dependency-version: 4.11.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: lucide-react
  dependency-version: 0.563.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: react-router
  dependency-version: 7.13.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: recharts
  dependency-version: 3.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: "@happy-dom/global-registrator"
  dependency-version: 20.3.7
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@hey-api/openapi-ts"
  dependency-version: 0.90.9
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@playwright/test"
  dependency-version: 1.58.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: "@react-router/dev"
  dependency-version: 7.13.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: "@types/node"
  dependency-version: 25.0.10
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@types/react"
  dependency-version: 19.2.9
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: lightningcss
  dependency-version: 1.31.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: vite-tsconfig-paths
  dependency-version: 6.0.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore: bump hono version in overrides

* chore: gen api client

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Nicolas Meienberger <github@thisprops.com>
2026-01-25 11:22:29 +01:00
Nico
273aa2b9ac
feat: add a confirm dialog before stopping the backup (#409) 2026-01-25 10:20:23 +01:00
Nicolas Meienberger
1166fcb3e7 test: fix typing issue 2026-01-25 10:01:51 +01:00
Nicolas Meienberger
16ac608d46 fix(sftp-repo): password request prompted in console 2026-01-24 23:03:54 +01:00
Nicolas Meienberger
458799297d refactor: make sse events isolated per org 2026-01-24 22:45:56 +01:00
Nicolas Meienberger
17776606ee temp: test re-key 2fas
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2026-01-22 22:44:23 +01:00
Nicolas Meienberger
88fa06b503 fix: prevent path traversal in list volume files 2026-01-22 22:32:04 +01:00
Nicolas Meienberger
ffc43e0c25 refactor(sftp-backend): use node spawn to properly send password stdin instead of echo
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2026-01-22 22:07:10 +01:00
Nicolas Meienberger
0e5438897c fix(repo-mutex): catch errors and remove listener 2026-01-22 22:06:39 +01:00
Nico
51d2ffad17
refactor: mount smb by puttings credentials in temporary file (#396) 2026-01-22 21:56:20 +01:00
Nico
2ab37e6b67
refactor: async doctor (#375)
* feat: background doctor operation

* refactor(repo-details): design layout

* refactor(doctor): support abort signal in all operations

* chore: fix linting issue

* chore: pr feedbacks

* chore: merge conflicts

* refactor: handle aborted signal in all operations

* chore: pr feedbacks

* chore: remove old migration
2026-01-22 21:55:45 +01:00
Nico
da37b08fa0
feat: user management (#394)
* feat: user management settings

* feat: cleanup user's org when deleting them

* chore: pr feedback

* refactor(create-user): tanstack mutation
2026-01-21 22:25:15 +01:00
Nico
451aed8983
Multi users (#381)
* feat(db): add support for multiple users and organizations

* feat: backfill entities with new organization id

* refactor: filter all backend queries to surface only organization specific entities

* refactor: each org has its own restic password

* test: ensure organization is created

* chore: pr feedbacks

* refactor: filter by org id in all places

* refactor: download restic password from stored db password

* refactor(navigation): use volume id in urls instead of name

* feat: disable registrations

* refactor(auth): bubble up auth error to hono

* refactor: use async local storage for cleaner context sharing

* refactor: enable user registration vs disabling it

* test: multi-org isolation

* chore: final cleanup
2026-01-20 22:28:22 +01:00
dependabot[bot]
44b1a27dd1
chore(deps): bump the minor-patch group across 1 directory with 14 updates (#390)
Bumps the minor-patch group with 14 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@scalar/hono-api-reference](https://github.com/scalar/scalar/tree/HEAD/integrations/hono) | `0.9.33` | `0.9.34` |
| [@tanstack/react-query](https://github.com/TanStack/query/tree/HEAD/packages/react-query) | `5.90.17` | `5.90.19` |
| [better-auth](https://github.com/better-auth/better-auth/tree/HEAD/packages/better-auth) | `1.4.12` | `1.4.15` |
| [cron-parser](https://github.com/harrisiirak/cron-parser) | `5.4.0` | `5.5.0` |
| [es-toolkit](https://github.com/toss/es-toolkit) | `1.43.0` | `1.44.0` |
| [isbot](https://github.com/omrilotan/isbot) | `5.1.32` | `5.1.33` |
| [react-router-hono-server](https://github.com/rphlmr/react-router-hono-server) | `2.22.0` | `2.23.0` |
| [@happy-dom/global-registrator](https://github.com/capricorn86/happy-dom) | `20.1.0` | `20.3.3` |
| [@hey-api/openapi-ts](https://github.com/hey-api/openapi-ts) | `0.90.3` | `0.90.4` |
| [@testing-library/react](https://github.com/testing-library/react-testing-library) | `16.3.1` | `16.3.2` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.0.8` | `25.0.9` |
| [oxfmt](https://github.com/oxc-project/oxc/tree/HEAD/npm/oxfmt) | `0.24.0` | `0.26.0` |
| [oxlint](https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint) | `1.39.0` | `1.41.0` |
| [oxlint-tsgolint](https://github.com/oxc-project/tsgolint) | `0.11.0` | `0.11.1` |



Updates `@scalar/hono-api-reference` from 0.9.33 to 0.9.34
- [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-query` from 5.90.17 to 5.90.19
- [Release notes](https://github.com/TanStack/query/releases)
- [Changelog](https://github.com/TanStack/query/blob/main/packages/react-query/CHANGELOG.md)
- [Commits](https://github.com/TanStack/query/commits/@tanstack/react-query@5.90.19/packages/react-query)

Updates `better-auth` from 1.4.12 to 1.4.15
- [Release notes](https://github.com/better-auth/better-auth/releases)
- [Commits](https://github.com/better-auth/better-auth/commits/v1.4.15/packages/better-auth)

Updates `cron-parser` from 5.4.0 to 5.5.0
- [Release notes](https://github.com/harrisiirak/cron-parser/releases)
- [Commits](https://github.com/harrisiirak/cron-parser/compare/v5.4.0...v5.5.0)

Updates `es-toolkit` from 1.43.0 to 1.44.0
- [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.43.0...v1.44.0)

Updates `isbot` from 5.1.32 to 5.1.33
- [Changelog](https://github.com/omrilotan/isbot/blob/main/CHANGELOG.md)
- [Commits](https://github.com/omrilotan/isbot/compare/v5.1.32...v5.1.33)

Updates `react-router-hono-server` from 2.22.0 to 2.23.0
- [Release notes](https://github.com/rphlmr/react-router-hono-server/releases)
- [Changelog](https://github.com/rphlmr/react-router-hono-server/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rphlmr/react-router-hono-server/compare/v2.22.0...v2.23.0)

Updates `@happy-dom/global-registrator` from 20.1.0 to 20.3.3
- [Release notes](https://github.com/capricorn86/happy-dom/releases)
- [Commits](https://github.com/capricorn86/happy-dom/compare/v20.1.0...v20.3.3)

Updates `@hey-api/openapi-ts` from 0.90.3 to 0.90.4
- [Release notes](https://github.com/hey-api/openapi-ts/releases)
- [Changelog](https://github.com/hey-api/openapi-ts/blob/main/docs/CHANGELOG.md)
- [Commits](https://github.com/hey-api/openapi-ts/compare/@hey-api/openapi-ts@0.90.3...@hey-api/openapi-ts@0.90.4)

Updates `@testing-library/react` from 16.3.1 to 16.3.2
- [Release notes](https://github.com/testing-library/react-testing-library/releases)
- [Changelog](https://github.com/testing-library/react-testing-library/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/react-testing-library/compare/v16.3.1...v16.3.2)

Updates `@types/node` from 25.0.8 to 25.0.9
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `oxfmt` from 0.24.0 to 0.26.0
- [Release notes](https://github.com/oxc-project/oxc/releases)
- [Changelog](https://github.com/oxc-project/oxc/blob/main/npm/oxfmt/CHANGELOG.md)
- [Commits](https://github.com/oxc-project/oxc/commits/oxfmt_v0.26.0/npm/oxfmt)

Updates `oxlint` from 1.39.0 to 1.41.0
- [Release notes](https://github.com/oxc-project/oxc/releases)
- [Changelog](https://github.com/oxc-project/oxc/blob/main/npm/oxlint/CHANGELOG.md)
- [Commits](https://github.com/oxc-project/oxc/commits/oxlint_v1.41.0/npm/oxlint)

Updates `oxlint-tsgolint` from 0.11.0 to 0.11.1
- [Release notes](https://github.com/oxc-project/tsgolint/releases)
- [Commits](https://github.com/oxc-project/tsgolint/compare/v0.11.0...v0.11.1)

---
updated-dependencies:
- dependency-name: "@scalar/hono-api-reference"
  dependency-version: 0.9.34
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@tanstack/react-query"
  dependency-version: 5.90.19
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: better-auth
  dependency-version: 1.4.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: cron-parser
  dependency-version: 5.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: es-toolkit
  dependency-version: 1.44.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: isbot
  dependency-version: 5.1.33
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: react-router-hono-server
  dependency-version: 2.23.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: "@happy-dom/global-registrator"
  dependency-version: 20.3.3
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: "@hey-api/openapi-ts"
  dependency-version: 0.90.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@testing-library/react"
  dependency-version: 16.3.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@types/node"
  dependency-version: 25.0.9
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: oxfmt
  dependency-version: 0.26.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: oxlint
  dependency-version: 1.41.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: oxlint-tsgolint
  dependency-version: 0.11.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-20 18:06:32 +01:00
Raj Dave
bcd9e1a494
Add GitHub Issue templates (#383) 2026-01-20 06:34:17 +01:00
Moin Ahmad
d3118704ea
remove bun cache from production image to optimize the space (#380) 2026-01-17 15:17:00 +01:00
dependabot[bot]
36b17d73eb
chore(deps): bump actions/upload-artifact from 5 to 6 (#353)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 5 to 6.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-16 18:20:33 +01:00
dependabot[bot]
254bf04eea
chore(deps): bump actions/cache from 4 to 5 (#352)
Bumps [actions/cache](https://github.com/actions/cache) from 4 to 5.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-16 18:20:20 +01:00
dependabot[bot]
2ce4c5345d
chore(deps): bump actions/checkout from 4 to 6 (#354)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-16 18:20:00 +01:00
dependabot[bot]
71f6c03a80
chore(deps): bump the minor-patch group across 1 directory with 7 updates (#364)
* chore(deps): bump the minor-patch group across 1 directory with 7 updates

Bumps the minor-patch group with 7 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@hono/standard-validator](https://github.com/honojs/middleware/tree/HEAD/packages/standard-validator) | `0.2.1` | `0.2.2` |
| [@scalar/hono-api-reference](https://github.com/scalar/scalar/tree/HEAD/integrations/hono) | `0.9.32` | `0.9.33` |
| [@tanstack/react-query](https://github.com/TanStack/query/tree/HEAD/packages/react-query) | `5.90.16` | `5.90.17` |
| [hono](https://github.com/honojs/hono) | `4.11.3` | `4.11.4` |
| [react-hook-form](https://github.com/react-hook-form/react-hook-form) | `7.71.0` | `7.71.1` |
| [@types/bun](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/bun) | `1.3.5` | `1.3.6` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.0.7` | `25.0.8` |



Updates `@hono/standard-validator` from 0.2.1 to 0.2.2
- [Release notes](https://github.com/honojs/middleware/releases)
- [Changelog](https://github.com/honojs/middleware/blob/main/packages/standard-validator/CHANGELOG.md)
- [Commits](https://github.com/honojs/middleware/commits/@hono/standard-validator@0.2.2/packages/standard-validator)

Updates `@scalar/hono-api-reference` from 0.9.32 to 0.9.33
- [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-query` from 5.90.16 to 5.90.17
- [Release notes](https://github.com/TanStack/query/releases)
- [Changelog](https://github.com/TanStack/query/blob/main/packages/react-query/CHANGELOG.md)
- [Commits](https://github.com/TanStack/query/commits/@tanstack/react-query@5.90.17/packages/react-query)

Updates `hono` from 4.11.3 to 4.11.4
- [Release notes](https://github.com/honojs/hono/releases)
- [Commits](https://github.com/honojs/hono/compare/v4.11.3...v4.11.4)

Updates `react-hook-form` from 7.71.0 to 7.71.1
- [Release notes](https://github.com/react-hook-form/react-hook-form/releases)
- [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md)
- [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.71.0...v7.71.1)

Updates `@types/bun` from 1.3.5 to 1.3.6
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/bun)

Updates `@types/node` from 25.0.7 to 25.0.8
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@hono/standard-validator"
  dependency-version: 0.2.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@scalar/hono-api-reference"
  dependency-version: 0.9.33
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@tanstack/react-query"
  dependency-version: 5.90.17
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: hono
  dependency-version: 4.11.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: react-hook-form
  dependency-version: 7.71.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@types/bun"
  dependency-version: 1.3.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@types/node"
  dependency-version: 25.0.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* fix: sync hono override version with dependency update (#367)

* Initial plan

* fix: update hono override to ^4.11.4 to match dependency version

Co-authored-by: nicotsx <47644445+nicotsx@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: nicotsx <47644445+nicotsx@users.noreply.github.com>

* chore: update lock file

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: nicotsx <47644445+nicotsx@users.noreply.github.com>
Co-authored-by: Nicolas Meienberger <github@thisprops.com>
2026-01-15 22:16:36 +01:00
Nico
d3f9ced5dc
refactor: use spawn & exec instead of bun $ (#356)
* refactor: use spawn & exec accordingly

* chore: pr feedbacks
2026-01-15 22:09:00 +01:00
Nicolas Meienberger
2cbd0d1021 chore: bump bun to v1.3.6 2026-01-15 22:03:46 +01:00
Nicolas Meienberger
f4a222aa15 refactor(up/down limit): allow floating value & default to 1 2026-01-15 18:27:50 +01:00
Nicolas Meienberger
31719103f6 Merge branch 'Rajdave69-main' 2026-01-15 18:14:06 +01:00
Nicolas Meienberger
1622e34b0d refactoring(bw-limit): stylistic refactoring 2026-01-15 18:07:00 +01:00
dependabot[bot]
cb72e6c073 chore(deps): bump the minor-patch group with 9 updates (#355)
* chore(deps): bump the minor-patch group with 9 updates

Bumps the minor-patch group with 9 updates:

| Package | From | To |
| --- | --- | --- |
| [@inquirer/prompts](https://github.com/SBoudrias/Inquirer.js) | `8.1.0` | `8.2.0` |
| [better-auth](https://github.com/better-auth/better-auth/tree/HEAD/packages/better-auth) | `1.4.10` | `1.4.12` |
| [react-hook-form](https://github.com/react-hook-form/react-hook-form) | `7.70.0` | `7.71.0` |
| [@hey-api/openapi-ts](https://github.com/hey-api/openapi-ts) | `0.90.2` | `0.90.3` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.0.3` | `25.0.7` |
| [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) | `19.2.7` | `19.2.8` |
| [oxfmt](https://github.com/oxc-project/oxc/tree/HEAD/npm/oxfmt) | `0.23.0` | `0.24.0` |
| [oxlint](https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint) | `1.38.0` | `1.39.0` |
| [vite-tsconfig-paths](https://github.com/aleclarson/vite-tsconfig-paths) | `6.0.3` | `6.0.4` |


Updates `@inquirer/prompts` from 8.1.0 to 8.2.0
- [Release notes](https://github.com/SBoudrias/Inquirer.js/releases)
- [Commits](https://github.com/SBoudrias/Inquirer.js/compare/@inquirer/prompts@8.1.0...@inquirer/prompts@8.2.0)

Updates `better-auth` from 1.4.10 to 1.4.12
- [Release notes](https://github.com/better-auth/better-auth/releases)
- [Commits](https://github.com/better-auth/better-auth/commits/v1.4.12/packages/better-auth)

Updates `react-hook-form` from 7.70.0 to 7.71.0
- [Release notes](https://github.com/react-hook-form/react-hook-form/releases)
- [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md)
- [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.70.0...v7.71.0)

Updates `@hey-api/openapi-ts` from 0.90.2 to 0.90.3
- [Release notes](https://github.com/hey-api/openapi-ts/releases)
- [Changelog](https://github.com/hey-api/openapi-ts/blob/main/docs/CHANGELOG.md)
- [Commits](https://github.com/hey-api/openapi-ts/compare/@hey-api/openapi-ts@0.90.2...@hey-api/openapi-ts@0.90.3)

Updates `@types/node` from 25.0.3 to 25.0.7
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `@types/react` from 19.2.7 to 19.2.8
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

Updates `oxfmt` from 0.23.0 to 0.24.0
- [Release notes](https://github.com/oxc-project/oxc/releases)
- [Changelog](https://github.com/oxc-project/oxc/blob/main/npm/oxfmt/CHANGELOG.md)
- [Commits](https://github.com/oxc-project/oxc/commits/oxfmt_v0.24.0/npm/oxfmt)

Updates `oxlint` from 1.38.0 to 1.39.0
- [Release notes](https://github.com/oxc-project/oxc/releases)
- [Changelog](https://github.com/oxc-project/oxc/blob/main/npm/oxlint/CHANGELOG.md)
- [Commits](https://github.com/oxc-project/oxc/commits/oxlint_v1.39.0/npm/oxlint)

Updates `vite-tsconfig-paths` from 6.0.3 to 6.0.4
- [Release notes](https://github.com/aleclarson/vite-tsconfig-paths/releases)
- [Commits](https://github.com/aleclarson/vite-tsconfig-paths/compare/v6.0.3...v6.0.4)

---
updated-dependencies:
- dependency-name: "@inquirer/prompts"
  dependency-version: 8.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: better-auth
  dependency-version: 1.4.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: react-hook-form
  dependency-version: 7.71.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: "@hey-api/openapi-ts"
  dependency-version: 0.90.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@types/node"
  dependency-version: 25.0.7
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@types/react"
  dependency-version: 19.2.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: oxfmt
  dependency-version: 0.24.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: oxlint
  dependency-version: 1.39.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: vite-tsconfig-paths
  dependency-version: 6.0.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore: gen api client

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Nicolas Meienberger <github@thisprops.com>
2026-01-15 16:14:05 +01:00
Nicolas Meienberger
9fe134b93a docs(readme): update version 2026-01-15 16:14:05 +01:00
Nicolas Meienberger
02fe55b533 fix: properly catch before fallback mount 2026-01-15 16:14:05 +01:00
Nico
583393eb47 test(e2e): add container logs to report (#343) 2026-01-15 16:14:05 +01:00
Nico
9754e366c1 feat: add CLI command to change username (#342)
* feat: add CLI command to change username

* ci: fix wrong folder chmod
2026-01-15 16:14:05 +01:00
Nicolas Meienberger
52280be3c2 chore: lowercase all emails 2026-01-15 16:14:05 +01:00
Nicolas Meienberger
f0871ece52 chore: hide email in settings (not yet changeable) 2026-01-15 16:13:39 +01:00
Nico
04e824c332 e2e: test backup & restore functions (#341) 2026-01-15 16:13:39 +01:00
Nico
60c0ce208d test(e2e): admin user registration (#338)
* test(e2e): admin user registration

* ci: e2e workflow

* feat: disable rate limiting env var

* test(e2e): fix order of execution in registration tests

* ci: run e2e tests before release
2026-01-15 16:13:39 +01:00
Nicolas Meienberger
1462250727 chore: remove "about", "over", "almost" from timeago 2026-01-15 16:13:14 +01:00
Nicolas Meienberger
881e8f8abe feat: configurable trusted origins 2026-01-15 16:13:14 +01:00
Nicolas Meienberger
4fda10d89d style(snapshots-timeline): ensure same width for all cards 2026-01-15 16:13:14 +01:00
Nico
9bff375217 refactor: format dates in a human-friendly way (#334) 2026-01-15 16:13:14 +01:00
Nico
b9f92f7430 chore: add a pre-commit hook to format files (#336)
chore: add a pre-commit hook to format files
2026-01-15 16:13:14 +01:00
Jakub Trávník
2989e1c3e8 docs: add troubleshooting readme file (#321)
* docs: add troubleshooting section to README and create TROUBLESHOOTING.md

* Update TROUBLESHOOTING.md

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update TROUBLESHOOTING.md

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update TROUBLESHOOTING.md

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-15 16:13:14 +01:00
Raj Dave
623b4b7116 Remove 'Upload Limit' and 'Download Limit' text 2026-01-13 23:07:03 +03:00
dependabot[bot]
c75e5cb21f
chore(deps): bump the minor-patch group with 9 updates (#355)
* chore(deps): bump the minor-patch group with 9 updates

Bumps the minor-patch group with 9 updates:

| Package | From | To |
| --- | --- | --- |
| [@inquirer/prompts](https://github.com/SBoudrias/Inquirer.js) | `8.1.0` | `8.2.0` |
| [better-auth](https://github.com/better-auth/better-auth/tree/HEAD/packages/better-auth) | `1.4.10` | `1.4.12` |
| [react-hook-form](https://github.com/react-hook-form/react-hook-form) | `7.70.0` | `7.71.0` |
| [@hey-api/openapi-ts](https://github.com/hey-api/openapi-ts) | `0.90.2` | `0.90.3` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.0.3` | `25.0.7` |
| [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) | `19.2.7` | `19.2.8` |
| [oxfmt](https://github.com/oxc-project/oxc/tree/HEAD/npm/oxfmt) | `0.23.0` | `0.24.0` |
| [oxlint](https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint) | `1.38.0` | `1.39.0` |
| [vite-tsconfig-paths](https://github.com/aleclarson/vite-tsconfig-paths) | `6.0.3` | `6.0.4` |


Updates `@inquirer/prompts` from 8.1.0 to 8.2.0
- [Release notes](https://github.com/SBoudrias/Inquirer.js/releases)
- [Commits](https://github.com/SBoudrias/Inquirer.js/compare/@inquirer/prompts@8.1.0...@inquirer/prompts@8.2.0)

Updates `better-auth` from 1.4.10 to 1.4.12
- [Release notes](https://github.com/better-auth/better-auth/releases)
- [Commits](https://github.com/better-auth/better-auth/commits/v1.4.12/packages/better-auth)

Updates `react-hook-form` from 7.70.0 to 7.71.0
- [Release notes](https://github.com/react-hook-form/react-hook-form/releases)
- [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md)
- [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.70.0...v7.71.0)

Updates `@hey-api/openapi-ts` from 0.90.2 to 0.90.3
- [Release notes](https://github.com/hey-api/openapi-ts/releases)
- [Changelog](https://github.com/hey-api/openapi-ts/blob/main/docs/CHANGELOG.md)
- [Commits](https://github.com/hey-api/openapi-ts/compare/@hey-api/openapi-ts@0.90.2...@hey-api/openapi-ts@0.90.3)

Updates `@types/node` from 25.0.3 to 25.0.7
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `@types/react` from 19.2.7 to 19.2.8
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

Updates `oxfmt` from 0.23.0 to 0.24.0
- [Release notes](https://github.com/oxc-project/oxc/releases)
- [Changelog](https://github.com/oxc-project/oxc/blob/main/npm/oxfmt/CHANGELOG.md)
- [Commits](https://github.com/oxc-project/oxc/commits/oxfmt_v0.24.0/npm/oxfmt)

Updates `oxlint` from 1.38.0 to 1.39.0
- [Release notes](https://github.com/oxc-project/oxc/releases)
- [Changelog](https://github.com/oxc-project/oxc/blob/main/npm/oxlint/CHANGELOG.md)
- [Commits](https://github.com/oxc-project/oxc/commits/oxlint_v1.39.0/npm/oxlint)

Updates `vite-tsconfig-paths` from 6.0.3 to 6.0.4
- [Release notes](https://github.com/aleclarson/vite-tsconfig-paths/releases)
- [Commits](https://github.com/aleclarson/vite-tsconfig-paths/compare/v6.0.3...v6.0.4)

---
updated-dependencies:
- dependency-name: "@inquirer/prompts"
  dependency-version: 8.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: better-auth
  dependency-version: 1.4.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: react-hook-form
  dependency-version: 7.71.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: "@hey-api/openapi-ts"
  dependency-version: 0.90.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@types/node"
  dependency-version: 25.0.7
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@types/react"
  dependency-version: 19.2.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: oxfmt
  dependency-version: 0.24.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: oxlint
  dependency-version: 1.39.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: vite-tsconfig-paths
  dependency-version: 6.0.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore: gen api client

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Nicolas Meienberger <github@thisprops.com>
2026-01-13 07:52:27 +01:00
Nicolas Meienberger
e16317cf9f docs(readme): update version 2026-01-11 16:48:19 +01:00
Nicolas Meienberger
df164af0f2 fix: properly catch before fallback mount
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2026-01-11 16:26:26 +01:00
Nico
f130ca704c
test(e2e): add container logs to report (#343)
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2026-01-11 16:02:36 +01:00
Nico
b2098f6beb
feat: add CLI command to change username (#342)
* feat: add CLI command to change username

* ci: fix wrong folder chmod
2026-01-11 14:53:57 +01:00
Nicolas Meienberger
734975c006 chore: lowercase all emails 2026-01-11 14:06:46 +01:00
Nicolas Meienberger
fe08895ac3 chore: hide email in settings (not yet changeable) 2026-01-11 14:01:14 +01:00
Nico
1ea2460ea7
e2e: test backup & restore functions (#341) 2026-01-11 14:00:29 +01:00
Nico
bb2bdb9724
test(e2e): admin user registration (#338)
Some checks failed
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
* test(e2e): admin user registration

* ci: e2e workflow

* feat: disable rate limiting env var

* test(e2e): fix order of execution in registration tests

* ci: run e2e tests before release
2026-01-11 10:31:04 +01:00
Raj Dave
145c321fa1 Refines bandwidth limit handling for rclone copy
Simplifies bandwidth limit configuration for rclone source backends by considering only the source download limit.

Ensures correct type casting when comparing upload and download limits, using base 10 for parsing.

Makes the temporary keys cleanup function available for external use.
2026-01-10 18:34:34 +03:00
Raj Dave
07a925b9f3 final issue i promise 2026-01-10 17:33:46 +03:00
Raj Dave
51e0526622 more fixed issues 2026-01-10 17:19:02 +03:00
Nicolas Meienberger
c9a6a11831 chore: remove "about", "over", "almost" from timeago 2026-01-10 13:35:25 +01:00
Nicolas Meienberger
185ce22ce9 feat: configurable trusted origins
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2026-01-10 13:22:57 +01:00
Nicolas Meienberger
2bb11b0d33 style(snapshots-timeline): ensure same width for all cards 2026-01-10 13:10:26 +01:00
Nico
ef2d644146
refactor: format dates in a human-friendly way (#334) 2026-01-10 13:02:46 +01:00
Raj Dave
c91bce7814 fix more coderabbit issues 2026-01-10 14:47:58 +03:00
Raj Dave
a7a4ccef6b fix a lot of the coderabbit warnings 2026-01-10 14:23:39 +03:00
Raj Dave
db127afe5a Adds bandwidth limiting to repositories
Implements bandwidth limits for repository uploads and downloads.

Adds UI components to configure bandwidth limits.
Adds database fields to store bandwidth limit configuration.
Integrates bandwidth limiting into restic commands.
2026-01-10 14:03:45 +03:00
Nico
1074d022b3
chore: add a pre-commit hook to format files (#336)
chore: add a pre-commit hook to format files
2026-01-10 11:57:24 +01:00
Jakub Trávník
5b6900c4ec
docs: add troubleshooting readme file (#321)
* docs: add troubleshooting section to README and create TROUBLESHOOTING.md

* Update TROUBLESHOOTING.md

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update TROUBLESHOOTING.md

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update TROUBLESHOOTING.md

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-10 11:15:55 +01:00
dependabot[bot]
8ba855f4ca
chore(deps-dev): bump oxlint-tsgolint in the minor-patch group (#332)
Bumps the minor-patch group with 1 update: [oxlint-tsgolint](https://github.com/oxc-project/tsgolint).


Updates `oxlint-tsgolint` from 0.10.1 to 0.11.0
- [Release notes](https://github.com/oxc-project/tsgolint/releases)
- [Commits](https://github.com/oxc-project/tsgolint/compare/v0.10.1...v0.11.0)

---
updated-dependencies:
- dependency-name: oxlint-tsgolint
  dependency-version: 0.11.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-10 10:29:02 +01:00
Nicolas Meienberger
99b7f35d8e Merge branch 'to-cherry' 2026-01-09 21:27:25 +01:00
Nicolas Meienberger
6e68c75ef1 chore: various fixes
fix(create-schedule): expand folder button submitting the form

style(schedule-form): fix multi scrollbar on long file list
2026-01-09 21:25:38 +01:00
Nico
d9b150b34e
feat: add 2FA (TOTP) support (#326)
* feat: setup better-auth with 2fa

* feat(totp): frontend

* refactor: split dialogs into components

* feat: disable 2fa cli

* chore: fix liniting issues

* chore(deps): bump the minor-patch group across 1 directory with 19 updates (#327)

* chore(deps): bump the minor-patch group across 1 directory with 19 updates

Bumps the minor-patch group with 19 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@react-router/node](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-node) | `7.11.0` | `7.12.0` |
| [@react-router/serve](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-serve) | `7.11.0` | `7.12.0` |
| [@scalar/hono-api-reference](https://github.com/scalar/scalar/tree/HEAD/integrations/hono) | `0.9.30` | `0.9.32` |
| [@tanstack/react-query](https://github.com/TanStack/query/tree/HEAD/packages/react-query) | `5.90.12` | `5.90.16` |
| [drizzle-orm](https://github.com/drizzle-team/drizzle-orm) | `0.44.7` | `0.45.1` |
| [hono](https://github.com/honojs/hono) | `4.10.5` | `4.11.3` |
| [hono-rate-limiter](https://github.com/rhinobase/hono-rate-limiter) | `0.5.1` | `0.5.3` |
| [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `0.555.0` | `0.562.0` |
| [react-hook-form](https://github.com/react-hook-form/react-hook-form) | `7.69.0` | `7.70.0` |
| [react-router](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router) | `7.11.0` | `7.12.0` |
| [recharts](https://github.com/recharts/recharts) | `3.5.1` | `3.6.0` |
| [@faker-js/faker](https://github.com/faker-js/faker) | `10.1.0` | `10.2.0` |
| [@happy-dom/global-registrator](https://github.com/capricorn86/happy-dom) | `20.0.11` | `20.1.0` |
| [@hey-api/openapi-ts](https://github.com/hey-api/openapi-ts) | `0.88.2` | `0.90.2` |
| [@react-router/dev](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dev) | `7.11.0` | `7.12.0` |
| [@tanstack/react-query-devtools](https://github.com/TanStack/query/tree/HEAD/packages/react-query-devtools) | `5.91.1` | `5.91.2` |
| [oxfmt](https://github.com/oxc-project/oxc/tree/HEAD/npm/oxfmt) | `0.22.0` | `0.23.0` |
| [oxlint](https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint) | `1.36.0` | `1.38.0` |
| [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `7.3.0` | `7.3.1` |

Updates `@react-router/node` from 7.11.0 to 7.12.0
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-node/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/@react-router/node@7.12.0/packages/react-router-node)

Updates `@react-router/serve` from 7.11.0 to 7.12.0
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-serve/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/@react-router/serve@7.12.0/packages/react-router-serve)

Updates `@scalar/hono-api-reference` from 0.9.30 to 0.9.32
- [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-query` from 5.90.12 to 5.90.16
- [Release notes](https://github.com/TanStack/query/releases)
- [Changelog](https://github.com/TanStack/query/blob/main/packages/react-query/CHANGELOG.md)
- [Commits](https://github.com/TanStack/query/commits/@tanstack/react-query@5.90.16/packages/react-query)

Updates `drizzle-orm` from 0.44.7 to 0.45.1
- [Release notes](https://github.com/drizzle-team/drizzle-orm/releases)
- [Commits](https://github.com/drizzle-team/drizzle-orm/compare/0.44.7...0.45.1)

Updates `hono` from 4.10.5 to 4.11.3
- [Release notes](https://github.com/honojs/hono/releases)
- [Commits](https://github.com/honojs/hono/compare/v4.10.5...v4.11.3)

Updates `hono-rate-limiter` from 0.5.1 to 0.5.3
- [Release notes](https://github.com/rhinobase/hono-rate-limiter/releases)
- [Commits](https://github.com/rhinobase/hono-rate-limiter/compare/v0.5.1...v0.5.3)

Updates `lucide-react` from 0.555.0 to 0.562.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/0.562.0/packages/lucide-react)

Updates `react-hook-form` from 7.69.0 to 7.70.0
- [Release notes](https://github.com/react-hook-form/react-hook-form/releases)
- [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md)
- [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.69.0...v7.70.0)

Updates `react-router` from 7.11.0 to 7.12.0
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router@7.12.0/packages/react-router)

Updates `recharts` from 3.5.1 to 3.6.0
- [Release notes](https://github.com/recharts/recharts/releases)
- [Changelog](https://github.com/recharts/recharts/blob/main/CHANGELOG.md)
- [Commits](https://github.com/recharts/recharts/compare/v3.5.1...v3.6.0)

Updates `@faker-js/faker` from 10.1.0 to 10.2.0
- [Release notes](https://github.com/faker-js/faker/releases)
- [Changelog](https://github.com/faker-js/faker/blob/next/CHANGELOG.md)
- [Commits](https://github.com/faker-js/faker/compare/v10.1.0...v10.2.0)

Updates `@happy-dom/global-registrator` from 20.0.11 to 20.1.0
- [Release notes](https://github.com/capricorn86/happy-dom/releases)
- [Commits](https://github.com/capricorn86/happy-dom/compare/v20.0.11...v20.1.0)

Updates `@hey-api/openapi-ts` from 0.88.2 to 0.90.2
- [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.88.2...@hey-api/openapi-ts@0.90.2)

Updates `@react-router/dev` from 7.11.0 to 7.12.0
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-dev/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/@react-router/dev@7.12.0/packages/react-router-dev)

Updates `@tanstack/react-query-devtools` from 5.91.1 to 5.91.2
- [Release notes](https://github.com/TanStack/query/releases)
- [Changelog](https://github.com/TanStack/query/blob/main/packages/react-query-devtools/CHANGELOG.md)
- [Commits](https://github.com/TanStack/query/commits/@tanstack/react-query-devtools@5.91.2/packages/react-query-devtools)

Updates `oxfmt` from 0.22.0 to 0.23.0
- [Release notes](https://github.com/oxc-project/oxc/releases)
- [Changelog](https://github.com/oxc-project/oxc/blob/main/npm/oxfmt/CHANGELOG.md)
- [Commits](https://github.com/oxc-project/oxc/commits/oxfmt_v0.23.0/npm/oxfmt)

Updates `oxlint` from 1.36.0 to 1.38.0
- [Release notes](https://github.com/oxc-project/oxc/releases)
- [Changelog](https://github.com/oxc-project/oxc/blob/main/npm/oxlint/CHANGELOG.md)
- [Commits](https://github.com/oxc-project/oxc/commits/oxlint_v1.38.0/npm/oxlint)

Updates `vite` from 7.3.0 to 7.3.1
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v7.3.1/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.3.1/packages/vite)

---
updated-dependencies:
- dependency-name: "@react-router/node"
  dependency-version: 7.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: "@react-router/serve"
  dependency-version: 7.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: "@scalar/hono-api-reference"
  dependency-version: 0.9.32
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@tanstack/react-query"
  dependency-version: 5.90.16
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: drizzle-orm
  dependency-version: 0.45.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: hono
  dependency-version: 4.11.3
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: hono-rate-limiter
  dependency-version: 0.5.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: lucide-react
  dependency-version: 0.562.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: react-hook-form
  dependency-version: 7.70.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: react-router
  dependency-version: 7.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: recharts
  dependency-version: 3.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: "@faker-js/faker"
  dependency-version: 10.2.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: "@happy-dom/global-registrator"
  dependency-version: 20.1.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: "@hey-api/openapi-ts"
  dependency-version: 0.90.2
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: "@react-router/dev"
  dependency-version: 7.12.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: "@tanstack/react-query-devtools"
  dependency-version: 5.91.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: oxfmt
  dependency-version: 0.23.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: oxlint
  dependency-version: 1.38.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: vite
  dependency-version: 7.3.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore: downgrade hono

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Nicolas Meienberger <github@thisprops.com>

* chore: force hono version in transitive deps

* refactor: remove copy to clipboard everywhere as it's not possible on http

* chore: pr feedbacks

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-09 21:12:05 +01:00
Jakub Trávník
d0dd74a626
fix: update NFS version select to use controlled value (#329) 2026-01-09 18:37:24 +01:00
Jakub Trávník
1d2e737a98
fix: increase name length limit from 32 to 128 characters in backup schedule schema (#330) 2026-01-09 18:29:35 +01:00
Nicolas Meienberger
5fc17950ba chore: force hono version in transitive deps 2026-01-09 08:13:43 +01:00
dependabot[bot]
4e9ede0b02
chore(deps): bump the minor-patch group across 1 directory with 19 updates (#327)
* chore(deps): bump the minor-patch group across 1 directory with 19 updates

Bumps the minor-patch group with 19 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@react-router/node](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-node) | `7.11.0` | `7.12.0` |
| [@react-router/serve](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-serve) | `7.11.0` | `7.12.0` |
| [@scalar/hono-api-reference](https://github.com/scalar/scalar/tree/HEAD/integrations/hono) | `0.9.30` | `0.9.32` |
| [@tanstack/react-query](https://github.com/TanStack/query/tree/HEAD/packages/react-query) | `5.90.12` | `5.90.16` |
| [drizzle-orm](https://github.com/drizzle-team/drizzle-orm) | `0.44.7` | `0.45.1` |
| [hono](https://github.com/honojs/hono) | `4.10.5` | `4.11.3` |
| [hono-rate-limiter](https://github.com/rhinobase/hono-rate-limiter) | `0.5.1` | `0.5.3` |
| [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `0.555.0` | `0.562.0` |
| [react-hook-form](https://github.com/react-hook-form/react-hook-form) | `7.69.0` | `7.70.0` |
| [react-router](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router) | `7.11.0` | `7.12.0` |
| [recharts](https://github.com/recharts/recharts) | `3.5.1` | `3.6.0` |
| [@faker-js/faker](https://github.com/faker-js/faker) | `10.1.0` | `10.2.0` |
| [@happy-dom/global-registrator](https://github.com/capricorn86/happy-dom) | `20.0.11` | `20.1.0` |
| [@hey-api/openapi-ts](https://github.com/hey-api/openapi-ts) | `0.88.2` | `0.90.2` |
| [@react-router/dev](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dev) | `7.11.0` | `7.12.0` |
| [@tanstack/react-query-devtools](https://github.com/TanStack/query/tree/HEAD/packages/react-query-devtools) | `5.91.1` | `5.91.2` |
| [oxfmt](https://github.com/oxc-project/oxc/tree/HEAD/npm/oxfmt) | `0.22.0` | `0.23.0` |
| [oxlint](https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint) | `1.36.0` | `1.38.0` |
| [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `7.3.0` | `7.3.1` |



Updates `@react-router/node` from 7.11.0 to 7.12.0
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-node/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/@react-router/node@7.12.0/packages/react-router-node)

Updates `@react-router/serve` from 7.11.0 to 7.12.0
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-serve/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/@react-router/serve@7.12.0/packages/react-router-serve)

Updates `@scalar/hono-api-reference` from 0.9.30 to 0.9.32
- [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-query` from 5.90.12 to 5.90.16
- [Release notes](https://github.com/TanStack/query/releases)
- [Changelog](https://github.com/TanStack/query/blob/main/packages/react-query/CHANGELOG.md)
- [Commits](https://github.com/TanStack/query/commits/@tanstack/react-query@5.90.16/packages/react-query)

Updates `drizzle-orm` from 0.44.7 to 0.45.1
- [Release notes](https://github.com/drizzle-team/drizzle-orm/releases)
- [Commits](https://github.com/drizzle-team/drizzle-orm/compare/0.44.7...0.45.1)

Updates `hono` from 4.10.5 to 4.11.3
- [Release notes](https://github.com/honojs/hono/releases)
- [Commits](https://github.com/honojs/hono/compare/v4.10.5...v4.11.3)

Updates `hono-rate-limiter` from 0.5.1 to 0.5.3
- [Release notes](https://github.com/rhinobase/hono-rate-limiter/releases)
- [Commits](https://github.com/rhinobase/hono-rate-limiter/compare/v0.5.1...v0.5.3)

Updates `lucide-react` from 0.555.0 to 0.562.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/0.562.0/packages/lucide-react)

Updates `react-hook-form` from 7.69.0 to 7.70.0
- [Release notes](https://github.com/react-hook-form/react-hook-form/releases)
- [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md)
- [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.69.0...v7.70.0)

Updates `react-router` from 7.11.0 to 7.12.0
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router@7.12.0/packages/react-router)

Updates `recharts` from 3.5.1 to 3.6.0
- [Release notes](https://github.com/recharts/recharts/releases)
- [Changelog](https://github.com/recharts/recharts/blob/main/CHANGELOG.md)
- [Commits](https://github.com/recharts/recharts/compare/v3.5.1...v3.6.0)

Updates `@faker-js/faker` from 10.1.0 to 10.2.0
- [Release notes](https://github.com/faker-js/faker/releases)
- [Changelog](https://github.com/faker-js/faker/blob/next/CHANGELOG.md)
- [Commits](https://github.com/faker-js/faker/compare/v10.1.0...v10.2.0)

Updates `@happy-dom/global-registrator` from 20.0.11 to 20.1.0
- [Release notes](https://github.com/capricorn86/happy-dom/releases)
- [Commits](https://github.com/capricorn86/happy-dom/compare/v20.0.11...v20.1.0)

Updates `@hey-api/openapi-ts` from 0.88.2 to 0.90.2
- [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.88.2...@hey-api/openapi-ts@0.90.2)

Updates `@react-router/dev` from 7.11.0 to 7.12.0
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-dev/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/@react-router/dev@7.12.0/packages/react-router-dev)

Updates `@tanstack/react-query-devtools` from 5.91.1 to 5.91.2
- [Release notes](https://github.com/TanStack/query/releases)
- [Changelog](https://github.com/TanStack/query/blob/main/packages/react-query-devtools/CHANGELOG.md)
- [Commits](https://github.com/TanStack/query/commits/@tanstack/react-query-devtools@5.91.2/packages/react-query-devtools)

Updates `oxfmt` from 0.22.0 to 0.23.0
- [Release notes](https://github.com/oxc-project/oxc/releases)
- [Changelog](https://github.com/oxc-project/oxc/blob/main/npm/oxfmt/CHANGELOG.md)
- [Commits](https://github.com/oxc-project/oxc/commits/oxfmt_v0.23.0/npm/oxfmt)

Updates `oxlint` from 1.36.0 to 1.38.0
- [Release notes](https://github.com/oxc-project/oxc/releases)
- [Changelog](https://github.com/oxc-project/oxc/blob/main/npm/oxlint/CHANGELOG.md)
- [Commits](https://github.com/oxc-project/oxc/commits/oxlint_v1.38.0/npm/oxlint)

Updates `vite` from 7.3.0 to 7.3.1
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v7.3.1/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.3.1/packages/vite)

---
updated-dependencies:
- dependency-name: "@react-router/node"
  dependency-version: 7.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: "@react-router/serve"
  dependency-version: 7.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: "@scalar/hono-api-reference"
  dependency-version: 0.9.32
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@tanstack/react-query"
  dependency-version: 5.90.16
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: drizzle-orm
  dependency-version: 0.45.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: hono
  dependency-version: 4.11.3
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: hono-rate-limiter
  dependency-version: 0.5.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: lucide-react
  dependency-version: 0.562.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: react-hook-form
  dependency-version: 7.70.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: react-router
  dependency-version: 7.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: recharts
  dependency-version: 3.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: "@faker-js/faker"
  dependency-version: 10.2.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: "@happy-dom/global-registrator"
  dependency-version: 20.1.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: "@hey-api/openapi-ts"
  dependency-version: 0.90.2
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: "@react-router/dev"
  dependency-version: 7.12.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: "@tanstack/react-query-devtools"
  dependency-version: 5.91.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: oxfmt
  dependency-version: 0.23.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: oxlint
  dependency-version: 1.38.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: vite
  dependency-version: 7.3.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore: downgrade hono

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Nicolas Meienberger <github@thisprops.com>
2026-01-09 08:11:05 +01:00
Nicolas Meienberger
8a05423153 fix: fallback to use non mount helper in case of failed mount 2026-01-08 21:44:03 +01:00
Nicolas Meienberger
a44fea0737 chore: add back nfs-utils
Some checks failed
Release Workflow / checks (push) Has been cancelled
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2026-01-08 21:05:22 +01:00
Nicolas Meienberger
1866db1969 Merge branch 'tvarohohlavy-defer-auth-after-passfile' 2026-01-08 20:50:44 +01:00
Nicolas Meienberger
a4464d243a refactor: auth to proxy pattern 2026-01-08 20:50:08 +01:00
Nicolas Meienberger
10b85fdd42 chore: verbose logging for mount commands 2026-01-08 20:35:14 +01:00
Nico
a25d303d54 chore: add more linting rules and fix warnings (#325) 2026-01-08 20:35:14 +01:00
Nico
72a0c2e923 refactor: implement a proper code migration system (#324)
* refactor: implement a proper code migration system

* chore: rename .tsx -> .ts
2026-01-08 20:35:14 +01:00
Nicolas Meienberger
76f494c8be chore: verbose logging for mount commands 2026-01-08 20:04:27 +01:00
Nico
87403c9407
chore: add more linting rules and fix warnings (#325) 2026-01-08 18:53:02 +01:00
Nico
76741d6fb8
refactor: implement a proper code migration system (#324)
* refactor: implement a proper code migration system

* chore: rename .tsx -> .ts
2026-01-08 18:46:32 +01:00
Jakub Trávník
87d267a1c3 static import and error catching 2026-01-08 15:22:20 +01:00
Jakub Trávník
e6793c9655 refactor: restructure auth initialization to fix type errors 2026-01-08 15:13:30 +01:00
Jakub Trávník
89315bb8a1 fix: defer auth initialization until after passfile exists 2026-01-08 14:57:06 +01:00
Nico
99932a8522
refactor: better-auth (#319)
* refactor: better-auth

* chore: pr feedback

* chore: lower + trim usernames in db
2026-01-07 22:36:20 +01:00
Nicolas Meienberger
8f915ea8bf ci: don't block alpha and beta releases on grype scans
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2026-01-07 21:15:17 +01:00
Nico
52550f24b2
fix: include full nfs/cifs utils to fix mounting issues in older protocol versions (#320) 2026-01-07 19:35:01 +01:00
Nicolas Meienberger
d05123b6d4 Merge branch 'tvarohohlavy-line-endings' 2026-01-06 18:42:23 +01:00
Nicolas Meienberger
9c253cce2c chore: end of line in oxfmtrc 2026-01-06 18:35:25 +01:00
Nico
f0e956783a 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
2026-01-06 18:34:10 +01:00
Nicolas Meienberger
13428224e8 chore: remove needless comments 2026-01-06 18:34:10 +01:00
Jakub Trávník
61aaa8938a fix: handle imported repositories correctly in getEffectiveLocalPath
- Imported repositories use config.path directly (matching server logic)
- Removed unused isImportedLocal variable
2026-01-06 18:34:10 +01:00
Jakub Trávník
4beb521c52 fix(info): optimize effective local path retrieval in RepositoryInfoTabContent 2026-01-06 18:34:10 +01:00
Jakub Trávník
c9dc663104 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
2026-01-06 18:34:06 +01:00
Nico
1391d6fe5e 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 525dcd8d9f.
2026-01-06 18:34:06 +01:00
Nico
7f71767ce7 refactor(notifications): add more context in title (#296)
* refactor(notifications): add more context in title

* chore: pr feedback
2026-01-06 18:34:00 +01:00
Nico
62de036847
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
2026-01-05 21:10:00 +01:00
Nicolas Meienberger
3467d1e411 Merge branch 'tvarohohlavy-local-repo-enhancements' 2026-01-05 21:06:59 +01:00
Nicolas Meienberger
b7609a6574 chore: remove needless comments 2026-01-05 21:06:36 +01:00
Nico
b216219209
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 525dcd8d9f.
2026-01-05 21:04:37 +01:00
Nico
ebbf3391a7
refactor(notifications): add more context in title (#296)
* refactor(notifications): add more context in title

* chore: pr feedback
2026-01-05 20:46:36 +01:00
Jakub Trávník
4671ebfc79
added more explicit text file types
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-05 10:27:39 +01:00
Jakub Trávník
f04d976cd8
changed to tab_width
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-05 10:26:53 +01:00
Jakub Trávník
9582622ed2 feat: add .editorconfig and .gitattributes and edited biome.json for consistent formatting and line endings 2026-01-05 10:17:22 +01:00
Jakub Trávník
86263fede7
Merge branch 'main' into local-repo-enhancements 2026-01-05 08:18:31 +01:00
Nico
21040d7d4c
feat: detect if a custom docker hostname was set before defaulting to zerobyte (#298) 2026-01-05 07:07:35 +01:00
Nico
d293738bf4
feat: show components versions (#297)
On hover on the Zerobyte version in the sidebar
A card will appear to show the versions of the
binaries (restic, rclone, shoutrrr)
2026-01-04 21:43:39 +01:00
Nicolas Meienberger
d6a5c1f10c docs(readme): add section about configurations 2026-01-04 20:56:06 +01:00
Nico
a2ee223ee7
refactor: use backend cache in restic operations (#295) 2026-01-04 16:37:42 +01:00
Nicolas Meienberger
79d5f47b19 chore(repo-form): style adjustments 2026-01-04 15:40:09 +01:00
Nicolas Meienberger
f67ca4eed2 style(delete notification): remove icon in delete button 2026-01-04 10:30:36 +01:00
Nico
d7bcf4c9e2
feat: display updates and release notes (#290)
* feat: display notification when new release is available

* refactor: standalone re-usable cache util

* refactor: clear cache on server startup

* refactor: add timeout to gh release fetch

* fix: run with app version env variable
2026-01-04 10:19:23 +01:00
Nicolas Meienberger
80acbbabe1 docs: update version in readme 2026-01-04 07:31:40 +01:00
Jakub Trávník
2d6c9397c0 fix: handle imported repositories correctly in getEffectiveLocalPath
- Imported repositories use config.path directly (matching server logic)
- Removed unused isImportedLocal variable
2026-01-03 12:47:03 +01:00
Jakub Trávník
affe0a45fa Merge origin/main into local-repo-enhancements 2026-01-03 12:42:53 +01:00
Nicolas Meienberger
1fff5f53c5 chore: fix migration version
Some checks failed
Release Workflow / checks (push) Has been cancelled
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2026-01-03 00:26:57 +01:00
Nicolas Meienberger
3eb07ef924 refactor: allow the server to start, even in case of migration failure
Some checks failed
Release Workflow / checks (push) Has been cancelled
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2026-01-03 00:16:42 +01:00
Nico
b0a1e4b3d7
fix: also migrate tag for schedule mirrors (#283)
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2026-01-02 23:07:14 +01:00
Nico
1b88fa6bd6
style: responsive fixes (#282)
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
* style(backups): use container queries to avoid overflow when menu is open

* style: fix breadcrumbs breaking the layout on smaller devices

* style: improve wording and error displays

* chore: wording
2026-01-02 22:10:27 +01:00
Nico
b8fe2a985b
feat: insecure tls & cacert for self-hosted repos (#277)
* feat: insecure tls & cacert for self-hosted repos

* fix: extra arg
2026-01-02 18:13:19 +01:00
Nicolas Meienberger
08e43e1f07 Merge branch 'tvories-feat/rest-tls-support' 2026-01-02 17:37:56 +01:00
Nicolas Meienberger
e0027b7668 refactor: code style and api client 2026-01-02 17:37:38 +01:00
tvories
08a19ed38d fix: PR suggestions 2026-01-02 17:05:43 +01:00
tvories
1a12436195 fix: add property existence checks before accessing repository config fields 2026-01-02 17:05:43 +01:00
tvories
e78af76beb feat: added additional validation, moved field location, made skip tls and ca certificate mutually exclusive with tooltips 2026-01-02 17:05:43 +01:00
tvories
07e68b9fe7 feat: add support for a cacert configuration as well as allowing the user to skip tls validation 2026-01-02 17:05:43 +01:00
Nico
2c786dfb35
feat: snapshots bulk actions (#256)
* feat: delete snapshots in bulk

* feat: re-tag snapshots in bulk

* chore: pr feedbacks
2026-01-02 17:04:06 +01:00
Nico
013fe5a521
Fix/file tree selection parents (#276)
* fix(file-tree): propagate path simplification to parents

When all childs of a folder are selected in the tree, the selected path
was only simplifiyng one level. But if the reduction was also making its
own parent "fully selected" it was not correctly reporting only the
parent as selected

* test(file-tree): ensure path selection correctness

* fix(file-tree): cleanup based on given path not what's visible
2026-01-02 16:56:13 +01:00
Jakub Trávník
f836e2f9df
fix(backups): allow removing selected paths that no longer exist on filesystem (#272)
Add X button to selected paths list in backup schedule form. When files/folders
are selected for backup but later deleted from the volume, restic fails because
`--files-from` references non-existent paths. Previously, users couldn't remove
these stale paths since they don't appear in the file browser to uncheck.

The X button enables direct removal of any selected path, fixing stuck backup
configurations that would always fail with "file not found" errors.
2026-01-02 15:33:25 +01:00
Nicolas Meienberger
5d7c6bd543 fix: cli not running with prod image 2026-01-02 15:22:44 +01:00
Nico
658c42a53b
feat: add generic webhook form & refactor each type in its own form (#239)
* feat: add generic webhook form & refactor each type in its own form

* chore: pr feedbacks

* fix(email-form): filter out empty emails
2026-01-02 14:56:35 +01:00
Nicolas Meienberger
b0c397ab87 Merge branch 'SaarLAN-Pissbeutel-ntfy-access-token' 2026-01-02 14:24:43 +01:00
Nicolas Meienberger
d535f02536 refactor: code readability 2026-01-02 14:24:22 +01:00
Jakub Trávník
94c84d7c58
feat: include schedule name in backup notifications and logs (#215)
* feat: include schedule name in backup notifications and logs

* lint fixes
2026-01-02 14:22:03 +01:00
SaarLAN-Pissbeutel
a5242fa64a
code cleanup
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-12-30 13:06:11 +01:00
SaarLAN-Pissbeutel
15fe9b25bd code cleanup 2025-12-30 11:56:13 +00:00
SaarLAN-Pissbeutel
1b8154a74b add access token for ntfy notifications 2025-12-30 11:08:32 +00:00
James Brink
eded452c83
feat: parameterize PORT and MIGRATIONS_PATH environment variables (#253)
* feat: parameterize PORT and MIGRATIONS_PATH environment variables

Allow configuring the server port (default: 4096) and migrations
folder path via environment variables for flexible deployment.

* fix: address PR review feedback

- Use consistent schema syntax (string? instead of key?)
- Remove config tests per maintainer request
2025-12-29 20:59:38 +01:00
Nico
90f2c0ac06
fix: boot with tini to correctly reap subprocesses and avoid zombies (#258) 2025-12-29 19:11:08 +01:00
Nico
c05aa8d5bf
feat: add support for SFTP volume backend (#249)
* feat: add support for SFTP volume backend

* feat: allow more secure SFTP connection with known hosts

* refactor: make default value for skip host key check to false

* refactor: use os.userInfo when setting uid/gid in mount commands
2025-12-28 18:30:49 +01:00
Nico
ae5233d9fb
refactor: tag snapshots with schedule short_id instead of db id (#248)
* refactor: tag snapshots with schedule short_id instead of db id

* chore: formatting issues
2025-12-28 11:42:25 +01:00
Nico
54068d5269
feat: allow changing the hostname (restic host) (#240) 2025-12-26 21:02:16 +01:00
Nicolas Meienberger
d5c0f91a9f ci: call checks before building docker image 2025-12-26 19:53:11 +01:00
Nicolas Meienberger
52a6ed6070 refactor: import local repositories use path only and not name
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2025-12-26 17:29:42 +01:00
Nicolas Meienberger
3ecf006977 refactor(login): extract reset password dialog to its own component 2025-12-26 17:13:26 +01:00
Jakub Trávník
a921fa4c80 fix(info): optimize effective local path retrieval in RepositoryInfoTabContent 2025-12-23 12:37:12 +01:00
Jakub Trávník
cd9f5a9bbe 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
2025-12-23 12:23:01 +01:00
Nicolas Meienberger
3a54f11b6d Merge branch 'tvarohohlavy-snapshots-table-and-detail-enhancement'
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2025-12-22 21:19:43 +01:00
Nicolas Meienberger
5cc7450ad0 refactor(snapshot-details): make page load faster by not awaiting restic snapshot details 2025-12-22 21:18:56 +01:00
Nico
00f44e4716 fix: allow arbitrary name for repositories and correctly import existing local repos (#197)
* fix: allow arbitrary name for repositories and correctly import existing local repos

* chore: rebase conflicts
2025-12-22 20:52:26 +01:00
Nico
4e0acb5856 feat(schedule-form): allow custom cron expression (#214)
* feat(schedule-form): allow custom cron expression

* refactor: guard against potential undefined cron expression
2025-12-22 20:51:46 +01:00
Nicolas Meienberger
59e0dc0401 chore: small code style changes 2025-12-22 20:51:46 +01:00
Jakub Trávník
64a584f3cd corrected webdav default path to match placeholder 2025-12-22 20:51:46 +01:00
Jakub Trávník
4dd9b7ffcc fix: add missing path property to webdav and rclone default values 2025-12-22 20:51:46 +01:00
Nico
5f01d6f10d feat: reset password cli command (#220)
* feat: reset password cli command

* refactor: wrap user password reset operations in db transaction
2025-12-22 20:51:46 +01:00
Nico
21b0c5e59c feat: server timeout env (#219)
* feat: increase server idle timeout and allow to modify it via env

* chore(app): move auth middleware to individual controller

To clean up de main app.ts file

* chore: remove console.log
2025-12-22 20:51:46 +01:00
Jakub Trávník
cf9357b4f1 feat: display file size in the File component 2025-12-22 20:51:46 +01:00
Nico
ac9de54a7b
fix: allow arbitrary name for repositories and correctly import existing local repos (#197)
* fix: allow arbitrary name for repositories and correctly import existing local repos

* chore: rebase conflicts
2025-12-22 20:50:53 +01:00
Nico
c6b569bed6
feat(schedule-form): allow custom cron expression (#214)
* feat(schedule-form): allow custom cron expression

* refactor: guard against potential undefined cron expression
2025-12-22 19:50:26 +01:00
Nicolas Meienberger
0b050ae06d Merge branch 'tvarohohlavy-file-tree-file-size' 2025-12-22 19:35:21 +01:00
Nicolas Meienberger
01f247c9a0 chore: small code style changes 2025-12-22 19:35:10 +01:00
Nico
91e76ec902
Merge branch 'main' into file-tree-file-size 2025-12-22 19:33:11 +01:00
Nicolas Meienberger
fb43bcff49 Merge branch 'tvarohohlavy-defualt-path-webdav-rclone' 2025-12-22 19:24:56 +01:00
Nico
6cd46445d2 feat: server timeout env (#219)
* feat: increase server idle timeout and allow to modify it via env

* chore(app): move auth middleware to individual controller

To clean up de main app.ts file

* chore: remove console.log
2025-12-22 19:23:52 +01:00
Nico
c69d2b6445
feat: reset password cli command (#220)
* feat: reset password cli command

* refactor: wrap user password reset operations in db transaction
2025-12-22 19:20:37 +01:00
Jakub Trávník
9c8dafd482 feat: display file size in the File component 2025-12-22 17:57:07 +01:00
Nico
55e27ef0b5
feat: server timeout env (#219)
* feat: increase server idle timeout and allow to modify it via env

* chore(app): move auth middleware to individual controller

To clean up de main app.ts file

* chore: remove console.log
2025-12-22 17:34:27 +01:00
Jakub Trávník
dccb38c70b corrected webdav default path to match placeholder 2025-12-22 15:39:05 +01:00
Jakub Trávník
ff9cfeda78 fix: add missing path property to webdav and rclone default values 2025-12-22 15:17:05 +01:00
Jakub Trávník
65655ebb56 fix: ensure safe access to backup name and volume in snapshot filtering 2025-12-22 14:35:37 +01:00
Jakub Trávník
9caccc969c
Update app/client/components/snapshots-table.tsx
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-22 14:26:39 +01:00
Jakub Trávník
068f9d88b3 feat: display backup schedule and volume information in snapshot details 2025-12-22 14:17:34 +01:00
Jakub Trávník
542842d012 feat: enhance snapshot filtering by including backup schedule names and volumes 2025-12-22 14:17:19 +01:00
Jakub Trávník
299469728e feat: add volume column to snapshots table and display backup volume names 2025-12-22 14:16:57 +01:00
Jakub Trávník
e992c6739a fix: update backup display name in snapshots table 2025-12-22 14:16:20 +01:00
Nicolas Meienberger
f952532290 Merge branch 'alexzapd-montly-schedule2' 2025-12-21 20:38:08 +01:00
Nicolas Meienberger
a985c95a31 chore: wording and formatting 2025-12-21 20:37:34 +01:00
Alessandro Zappia
e350843d4e Update app/utils/utils.ts
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-12-21 20:36:13 +01:00
Alessandro Zappia
47aefa9bcb Update app/utils/utils.ts
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-12-21 20:36:13 +01:00
Alessandro Zappia
11f2c27f7f add monthly backup frequency with multi-day selection 2025-12-21 20:36:13 +01:00
dependabot[bot]
fe8eb1326d
chore(deps-dev): bump @types/node from 24.10.4 to 25.0.3 (#209)
* chore(deps-dev): bump @types/node from 24.10.4 to 25.0.3

Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 24.10.4 to 25.0.3.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 25.0.3
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore: use rm instead of rmdir

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Nicolas Meienberger <github@thisprops.com>
2025-12-21 19:51:03 +01:00
dependabot[bot]
91f1667158
chore(deps-dev): bump vite-tsconfig-paths from 5.1.4 to 6.0.3 (#210)
Bumps [vite-tsconfig-paths](https://github.com/aleclarson/vite-tsconfig-paths) from 5.1.4 to 6.0.3.
- [Release notes](https://github.com/aleclarson/vite-tsconfig-paths/releases)
- [Commits](https://github.com/aleclarson/vite-tsconfig-paths/compare/v5.1.4...v6.0.3)

---
updated-dependencies:
- dependency-name: vite-tsconfig-paths
  dependency-version: 6.0.3
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-21 19:47:16 +01:00
Nicolas Meienberger
c426bdcbf8 fix: sftp keep alive 2025-12-21 15:37:21 +01:00
dependabot[bot]
f926d5ec1f
chore(deps): bump actions/checkout from 5 to 6 (#204)
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-21 15:36:24 +01:00
Nico
f7822527bc
Change package-ecosystem from npm to bun 2025-12-21 15:29:25 +01:00
Nicolas Meienberger
6cc10293e1 fix: migration using 1 instead of true 2025-12-21 15:27:12 +01:00
Nico
1d7897b745
Configure Dependabot for npm and GitHub Actions
Updated Dependabot configuration to include npm and GitHub Actions with daily update schedules.
2025-12-21 15:13:54 +01:00
Nico
ea9d867cd2
feat: version link to gh release in sidebar (#203)
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2025-12-21 14:39:30 +01:00
Nico
8cdd06ec49
fix(stop): always update status to warning when stop is executed (#202)
* fix(stop): always update status to warning when stop is executed

* fix: automatically put in_progress backups to warning during startup
2025-12-21 14:25:45 +01:00
Nico
492aa4178d
feat: allow to control --one-file-system option from the schedule config (#201) 2025-12-21 14:21:08 +01:00
Nico
fc04cfe02e
refactor(restic): use safeSpawn util instead of Bun shell (#195)
Bun has an issue with special non-ASCII charachters
https://github.com/oven-sh/bun/issues/15929 that makes commands crash if
manipulating a file with such charachter
2025-12-20 11:47:45 +01:00
Nico
598de4e078
fix: schedules not running if never run manually (#194)
* fix: backups that were never run manually are not picked up by job

* fix(mutex): prioritize waiting exclusive locks over newly added shared tasks
2025-12-20 11:28:18 +01:00
Nicolas Meienberger
24fd18825a Merge branch 'tvarohohlavy-examples-directory' 2025-12-20 01:05:22 +01:00
Nicolas Meienberger
fcc49fb3ae docs: examples 2025-12-20 01:05:12 +01:00
Nicolas Meienberger
2564add45a ci(release): add permission to write content (create new release) 2025-12-20 01:04:48 +01:00
Nicolas Meienberger
9d0441f5d0 docs: update readme 2025-12-20 01:04:48 +01:00
Nicolas Meienberger
aeec2d7d4f chore(docs): update README 2025-12-20 01:04:48 +01:00
Nicolas Meienberger
c6657d44f2 docs(readme): add warning for running on public servers 2025-12-20 01:04:48 +01:00
Nicolas Meienberger
601d3a1143 refactor: warning only when backup is aborted 2025-12-20 01:04:48 +01:00
Nicolas Meienberger
09805ce5ba fix: ensure proper error message when backup operation is aborted 2025-12-20 01:04:48 +01:00
Nicolas Meienberger
0323ff0031 fix(backups): preserve relative include/exclude pattern behavior 2025-12-20 01:04:48 +01:00
Jakub Trávník
80cf8dd778 fix: include name in initial values for notification form update (#184) 2025-12-20 01:04:48 +01:00
Nico
3374e4f53d fix: prepend mount path in exclude patterns (#188)
* fix: prepend mount path location in exclude patterns

* test(backups): add tests suite for backend include & exclude patterns

* test: fix leaking global module mock
2025-12-20 01:04:48 +01:00
Nico
e9fb5862f7 Controllers tests (#187)
* test: backups service

* refactor: create hono app in a separate file

To avoid side effects like db migration or startup scripts when testing

test(backups): add security tests to the backups controller

* ci: run typechecks, build and tests on PR

* test: controllers security tests

* chore: update lock file

* refactor: pr feedbacks
2025-12-20 01:04:47 +01:00
Nicolas Meienberger
6266f1d44c ci(release): add permission to write content (create new release)
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2025-12-20 00:42:34 +01:00
Jakub Trávník
11b588ce20 readded some examples in main readme 2025-12-19 23:38:39 +00:00
Nicolas Meienberger
dea07338bc docs: update readme 2025-12-20 00:19:01 +01:00
Nicolas Meienberger
17e33ae98c chore(docs): update README 2025-12-20 00:14:34 +01:00
Nicolas Meienberger
6f302f8518 docs(readme): add warning for running on public servers 2025-12-20 00:13:33 +01:00
Nicolas Meienberger
febd795517 refactor: warning only when backup is aborted
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2025-12-19 23:30:23 +01:00
Nicolas Meienberger
46c8139a23 fix: ensure proper error message when backup operation is aborted
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2025-12-19 23:27:25 +01:00
Nicolas Meienberger
7879d88a54 fix(backups): preserve relative include/exclude pattern behavior 2025-12-19 23:18:56 +01:00
Jakub Trávník
30685c8481
fix: include name in initial values for notification form update (#184)
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2025-12-19 22:40:07 +01:00
Nico
579bc89970
fix: prepend mount path in exclude patterns (#188)
* fix: prepend mount path location in exclude patterns

* test(backups): add tests suite for backend include & exclude patterns

* test: fix leaking global module mock
2025-12-19 22:35:34 +01:00
Nico
61dc07b36b
Controllers tests (#187)
* test: backups service

* refactor: create hono app in a separate file

To avoid side effects like db migration or startup scripts when testing

test(backups): add security tests to the backups controller

* ci: run typechecks, build and tests on PR

* test: controllers security tests

* chore: update lock file

* refactor: pr feedbacks
2025-12-19 19:25:21 +01:00
Jakub Trávník
4e2f8f0160 implement copilot and rabbitai suggestions 2025-12-19 14:39:57 +01:00
Jakub Trávník
7cdf1d72e9
Update examples/tailscale-sidecar/README.md
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-19 14:27:09 +01:00
Jakub Trávník
89fbeb7fe3
Update examples/tailscale-sidecar/docker-compose.yml
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-19 14:26:49 +01:00
Jakub Trávník
9d46bbbcb1
Update examples/tailscale-sidecar/docker-compose.yml
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-19 14:26:10 +01:00
Jakub Trávník
7a6e3e9597 Merge remote-tracking branch 'origin/main' into tailscale-integration-example 2025-12-19 13:30:37 +01:00
Jakub Trávník
f995a2ca8d Add examples directory and runnable setups 2025-12-19 13:28:05 +01:00
Nico
94f49e2022
refactor: separate healthcheck and auto remount in two separate jobs (#179)
* refactor: separate healthcheck and auto remount in two separate jobs

* fix: try catch in volume auto remount
2025-12-18 23:03:41 +01:00
Nico
6a86de7b95
refactor: add default excludes to backups (#178)
- To avoid accidentaly backing up a repository to itself
- To avoid accidentaly backing up zerobyte's internal data
2025-12-18 22:34:21 +01:00
Nico
fd5193b5e3
refactor: reduce startup mounting noise (#177)
- Skip mount early if volume is not in error or neither mounted
- Error healthchecks early if volume is not mounted
2025-12-18 22:19:21 +01:00
Nico
a17da2562f
refactor(startup): ensure entities always use the latest configuration format (#173)
* refactor(startup): ensure entities always use the latest configuration schema

* refactor: await config updates to avoid race condition on later mount
2025-12-18 18:11:39 +01:00
Nicolas Meienberger
7a507354ad chore: upgrade apk in base image 2025-12-18 18:08:29 +01:00
Nico
55e68544f8
ci: add grype security checks before release and in pr (#175)
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
* ci: add grype security checks before release and in pr

* ci: only build for amd64 in local build
2025-12-18 18:02:34 +01:00
Jakub Trávník
be4c24ee3e feat: add Tailscale integration example with Docker Compose and environment configuration 2025-12-18 13:06:42 +01:00
Nicolas Meienberger
cac4c84df5 ci: add login to dockerhub step
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2025-12-18 12:33:42 +01:00
Nicolas Meienberger
83da66a0b7 ci: use docker cloud builder 2025-12-18 12:31:08 +01:00
Nicolas Meienberger
42f49fed23 chore(deps): bump rclone to 1.72.1 and shoutrrr to 0.13.1
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2025-12-18 12:16:08 +01:00
Nicolas Meienberger
933b66c87e chore(deps): enforce esbuild to 0.27.2 2025-12-18 12:12:46 +01:00
Nicolas Meienberger
4f9f167727 chore: remove sourcemaps from build output 2025-12-18 12:10:17 +01:00
Nicolas Meienberger
7951a87332 refactor: don't apply strict rate limit on /me and /status endpoints 2025-12-18 12:09:34 +01:00
Nicolas Meienberger
1bb9ccb33b Merge branch 'orbisai0security-fix-v-002-app-server-index.ts'
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2025-12-17 23:41:30 +01:00
Nicolas Meienberger
0838ce86bd refactor: remove cors options and fix rate limiter usage 2025-12-17 23:40:57 +01:00
Nicolas Meienberger
842464ce32 docs: update readme version 2025-12-17 23:35:47 +01:00
Nicolas Meienberger
45e7e88b09 fix: apply middlewares before the controller definition 2025-12-17 23:35:47 +01:00
Nicolas Meienberger
1d76e18d38 fix(sortable-card): keep opacity at 100 on mobile 2025-12-17 23:35:14 +01:00
Nicolas Meienberger
2d84b25664 docs: update readme version 2025-12-17 16:37:40 +01:00
Nicolas Meienberger
13e080a189 fix: apply middlewares before the controller definition
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2025-12-17 16:23:50 +01:00
Nicolas Meienberger
d8cfe29f22 fix(sortable-card): keep opacity at 100 on mobile 2025-12-17 09:00:55 +01:00
orbisai0security
3cebc1ce47 fix: resolve high vulnerability V-002
Automatically generated security fix
2025-12-17 07:47:05 +00:00
Nicolas Meienberger
1ea5c042f5 docs(readme): remove section about secrets and env (not released yet) 2025-12-17 07:27:47 +01:00
Nico
4ecb21e4ca Apply suggestion from @Copilot
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-17 07:26:32 +01:00
Nicolas Meienberger
f77fd86694 Merge branch 'tvarohohlavy-secrets-management'
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2025-12-16 20:47:29 +01:00
Nicolas Meienberger
47b3c80317 refactor(secret-input): remove isDirty check 2025-12-16 20:45:33 +01:00
Nicolas Meienberger
ec8fb2ebe5 Merge branch 'Rajdave69-main' 2025-12-16 20:24:00 +01:00
Nicolas Meienberger
e57efcfcdc refactor: extract sortable card into its own component 2025-12-16 20:23:44 +01:00
Nicolas Meienberger
b983add4ca chore(frontend): parametrize server IP 2025-12-16 19:40:53 +01:00
Nicolas Meienberger
e39552ee68 chore: small code style improvements 2025-12-16 19:36:37 +01:00
Nicolas Meienberger
feb6353e21 chore(frontend): parametrize server IP 2025-12-16 18:02:54 +01:00
Nicolas Meienberger
fe5810b1a7 fix: circular dependency 2025-12-16 18:02:54 +01:00
Nico
9e3cc90ba1 refactor: allow to run without the SYS_ADMIN cap for simple setups (#138)
* refactor: allow to run without the SYS_ADMIN cap for simple setups

* fix: pr feedback
2025-12-16 18:02:54 +01:00
Nicolas Meienberger
ebca72f1ee fix: username optional in snapshot output 2025-12-16 18:02:54 +01:00
Nico
1b6539c5bf refactor: remove docker volum plugin functionnality (#137) 2025-12-16 18:02:54 +01:00
Nicolas Meienberger
fe5c9ea599 refactor(rclone-form): minor UI improvements 2025-12-16 18:02:54 +01:00
Nico
dc20f209f7 fix: run forget on mirrors in the background (#136) 2025-12-16 18:02:54 +01:00
Nicolas Meienberger
affd300a09 fix(rclone-volume): remote was not loaded correctly in the form 2025-12-16 18:02:54 +01:00
Nico
54d077bf43 fix: same time backups run multiple times (#134) 2025-12-16 18:02:54 +01:00
Nico
3efff035c5 fix(doctor): fail gracefully in case of unexpected error (#132) 2025-12-16 18:02:54 +01:00
Nicolas Meienberger
c8962bb023 chore(frontend): parametrize server IP 2025-12-16 17:38:02 +01:00
Raj Dave
008af5487d Invalidates backup schedule queries on reorder
Ensures the backup schedule list is refreshed after reordering,
both on success and on error, by using the dedicated query key
instead of a generic string. This guarantees the UI reflects
the latest state.
2025-12-15 16:19:33 +03:00
Raj Dave
8e49b10042 Validates and improves schedule reordering
Ensures schedule reordering requests are valid by checking for duplicate and non-existent IDs.

Improves performance by using a transaction for batch updates.
2025-12-15 16:06:21 +03:00
Raj Dave
c3f5cd6b4e properly generate migration 2025-12-15 13:30:51 +03:00
Raj Dave
fa8e06a92e fix timestamp in drizzle journal 2025-12-15 13:26:47 +03:00
Raj Dave
b51f3549c2 Backend and UI modifications to allow reordering of backup schedules
Adds drag-and-drop functionality to reorder backup schedules

Introduces a sort order field to the database schema and implements API endpoints to update the schedule order.

This allows users to visually prioritize their backup jobs.
2025-12-15 13:13:34 +03:00
Jakub Trávník
c4ec8ea62b secret reference frontend usage 2025-12-15 10:50:24 +01:00
Jakub Trávník
58b1a0574e secret reference server usage 2025-12-15 10:50:13 +01:00
Jakub Trávník
605fb1c27c secret reference core support 2025-12-15 10:48:35 +01:00
Nicolas Meienberger
5216cc195d fix: circular dependency
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2025-12-14 20:49:27 +01:00
Nico
b23317354c
refactor: allow to run without the SYS_ADMIN cap for simple setups (#138)
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
* refactor: allow to run without the SYS_ADMIN cap for simple setups

* fix: pr feedback
2025-12-14 20:30:33 +01:00
Nicolas Meienberger
95e4fe5547 fix: username optional in snapshot output
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2025-12-14 20:25:42 +01:00
Nico
361311285a
refactor: remove docker volum plugin functionnality (#137) 2025-12-14 11:07:32 +01:00
Nicolas Meienberger
1eb247deec refactor(rclone-form): minor UI improvements 2025-12-14 10:44:05 +01:00
Nico
8a5d45cbfb
fix: run forget on mirrors in the background (#136) 2025-12-14 10:34:25 +01:00
Nicolas Meienberger
7619612b1b fix(rclone-volume): remote was not loaded correctly in the form 2025-12-14 10:33:16 +01:00
Nico
be53466739
fix: same time backups run multiple times (#134)
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2025-12-13 23:47:41 +01:00
Nico
6ab5c549c1
fix(doctor): fail gracefully in case of unexpected error (#132) 2025-12-12 17:55:37 +01:00
Nicolas Meienberger
47f64b51af docs: update version in readme
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2025-12-11 18:32:40 +01:00
Nico
c3acc26e05
refactor(notification): make username and password optional (smtp) (#126)
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2025-12-10 21:36:18 +01:00
Nico
ef8fd7228f
fix(mirrors): keep last copy state (#125) 2025-12-10 21:18:10 +01:00
Nico
2ddf3c6597
refactor: split forms (#124)
* refactor: split create-volume-form

* refactor: split create-repository-form

* chore: pr feedbacks
2025-12-10 21:16:20 +01:00
Nico
9199a743db
feat: rclone volume backend (#123) 2025-12-10 20:20:49 +01:00
Nico
4c0ba33b5e
feat: notify on warning separation (#122) 2025-12-10 18:55:37 +01:00
Nicolas Meienberger
890bdb8319 docs: update readme version 2025-12-09 07:14:29 +01:00
Nicolas Meienberger
7091f1f296 chore: remove retry-lock 2025-12-06 11:17:31 +01:00
Nicolas Meienberger
b394aca7ca Merge branch 'tvarohohlavy-missing-icons-2' 2025-12-06 11:13:51 +01:00
Nicolas Meienberger
fdb84374a0 refactor: remove icons where it was making the context worse 2025-12-06 11:13:26 +01:00
Nicolas Meienberger
c42380b26b refactor: remove trim on password
The password should be taken as-is. It could potentially contain a space
2025-12-06 10:36:39 +01:00
Nicolas Meienberger
51ed47c30f refactor: no need to print safe args as it's already sanitized 2025-12-06 10:36:39 +01:00
Nicolas Meienberger
b8ae10b316 fix: volume data not refreshing when changing selection 2025-12-06 10:36:39 +01:00
Nico
f232fc07c1 feat: custom include patterns (#104)
* feat: add custom include patterns

* feat: add exclude-if-present option
2025-12-06 10:36:39 +01:00
Nico
d542318e2c feat: naming backup schedules (#103)
* docs: add agents instructions

* feat: naming backup schedules

* fix: wrong table for filtering
2025-12-06 10:36:39 +01:00
Nicolas Meienberger
2660d91002 fix: broken migration 2025-12-06 10:36:25 +01:00
Nicolas Meienberger
b155f82575 chore: update dependencies 2025-12-06 10:36:25 +01:00
Nico
9120153375 feat: mirror repositories (#95)
* feat: mirror repositories

feat: mirror backup repositories

* chore: pr feedbacks
2025-12-06 10:36:17 +01:00
Jakub Trávník
780fdae63e cryptoUtils.decrypt alligned with encrypt in regards of handling extra space in password file 2025-12-06 10:35:55 +01:00
Jakub Trávník
fc482e9729 crypto.Utils comments updated to reflect behaviour 2025-12-06 10:35:55 +01:00
Jakub Trávník
1fe026a76f avoid logging secrets in smb backend 2025-12-06 10:35:55 +01:00
Jakub Trávník
2df1fa53a0 feat: implement encryption for sensitive fields in volume backends 2025-12-06 10:35:55 +01:00
Nicolas Meienberger
da8e9c4ada ci: fix docker cache args on wrong step 2025-12-06 10:35:55 +01:00
Nicolas Meienberger
da489fab24 Merge branch 'tvarohohlavy-volumes-secrets-encryption' 2025-12-06 10:08:20 +01:00
Nicolas Meienberger
e4b8076351 refactor: remove trim on password
The password should be taken as-is. It could potentially contain a space
2025-12-06 10:08:01 +01:00
Nicolas Meienberger
70c72f0f9a refactor: no need to print safe args as it's already sanitized 2025-12-06 10:06:03 +01:00
Nicolas Meienberger
c45b760abc ci: fix docker cache args on wrong step 2025-12-06 09:49:59 +01:00
Nicolas Meienberger
9ba26b7599 feat: add DOCKER_HOST support 2025-12-06 09:49:59 +01:00
Nicolas Meienberger
01127ee9d6 fix: volume data not refreshing when changing selection 2025-12-06 09:49:59 +01:00
Nicolas Meienberger
77f5886110 fix: remove debug logs in production 2025-12-06 09:49:59 +01:00
Nico
6b6338291b feat: custom include patterns (#104)
* feat: add custom include patterns

* feat: add exclude-if-present option
2025-12-06 09:49:59 +01:00
Nico
2c11b7c7de feat: naming backup schedules (#103)
* docs: add agents instructions

* feat: naming backup schedules

* fix: wrong table for filtering
2025-12-06 09:49:59 +01:00
Nicolas Meienberger
a0fa043207 fix: broken migration 2025-12-06 09:49:59 +01:00
Nicolas Meienberger
143701820a chore: update dependencies 2025-12-06 09:49:59 +01:00
Nico
aff875c62f feat: mirror repositories (#95)
* feat: mirror repositories

feat: mirror backup repositories

* chore: pr feedbacks
2025-12-06 09:49:59 +01:00
Nicolas Meienberger
e52c25d87b ci: fix docker cache args on wrong step 2025-12-06 09:39:10 +01:00
Jakub Trávník
4403e3cf19
Merge branch 'main' into missing-icons 2025-12-05 00:22:39 +01:00
Nicolas Meienberger
e85cc35b1a feat: add DOCKER_HOST support
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2025-12-04 19:09:23 +01:00
Nicolas Meienberger
321dc4cdf7 fix: volume data not refreshing when changing selection 2025-12-04 18:54:02 +01:00
Nicolas Meienberger
0f7bd1e042 fix: remove debug logs in production 2025-12-04 18:46:12 +01:00
Nico
08d8a47352
feat: custom include patterns (#104)
* feat: add custom include patterns

* feat: add exclude-if-present option
2025-12-04 18:44:34 +01:00
Nico
1e20fb225e
feat: naming backup schedules (#103)
* docs: add agents instructions

* feat: naming backup schedules

* fix: wrong table for filtering
2025-12-04 18:31:00 +01:00
Jakub Trávník
9fec6883f6 cryptoUtils.decrypt alligned with encrypt in regards of handling extra space in password file 2025-12-04 08:43:17 +01:00
Jakub Trávník
f4df9e935d crypto.Utils comments updated to reflect behaviour 2025-12-04 08:39:51 +01:00
Jakub Trávník
f326f41599 avoid logging secrets in smb backend 2025-12-04 08:33:55 +01:00
Jakub Trávník
f6b8e7e5a2 feat: implement encryption for sensitive fields in volume backends 2025-12-04 00:04:26 +01:00
Nicolas Meienberger
b8e30e298c fix: broken migration
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2025-12-03 21:56:41 +01:00
Nicolas Meienberger
ce07c588ad chore: update dependencies 2025-12-03 20:55:38 +01:00
Nico
e7f0a2828d
feat: mirror repositories (#95)
* feat: mirror repositories

feat: mirror backup repositories

* chore: pr feedbacks
2025-12-03 20:52:47 +01:00
Jakub Trávník
ff4c1404a6 feat: add icons to buttons and alerts for improved UI clarity 2025-12-03 17:31:44 +01:00
Nicolas Meienberger
7ff38f0128 refactor: run forget commands in the background
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2025-12-01 20:22:43 +01:00
Nicolas Meienberger
33e6f3773b fix: only lock shared tasks if an exclusive is running
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2025-12-01 20:07:30 +01:00
Nicolas Meienberger
a91dede086 docs: bump version in readme
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2025-12-01 19:48:55 +01:00
Nico
9b46737852
refactor(repositories): add a locking mechanism for restic operations (#94)
* refactor(repositories): add a locking mechanism for restic operations

* fix: add missing lock in list repositories
2025-12-01 19:47:21 +01:00
Nicolas Meienberger
999850dab8 Merge branch 'tvarohohlavy-telegram-notification' 2025-11-30 17:06:07 +01:00
Nicolas Meienberger
dbd9ae2241 chore: generate types 2025-11-30 17:05:40 +01:00
Nico
0287bca4bb restore as a page (#87)
* feat: add custom restore target directory

Adds the ability to restore snapshots to a custom directory instead of
only the original path. Closes #12.

Changes:
- Add target parameter to restore API endpoint
- Add directory picker UI in file browser restore dialog
- Add target input field in snapshot restore form
- Create reusable PathSelector component

Note: Run `bun run gen:api-client` after merging to regenerate types.

* refactor: path selector design

* refactor: unify restore snapshot dialogs

* refactor: restore snapshot as a page

* chore: fix liniting issues

* chore(create-notification): remove un-used prop

---------

Co-authored-by: Deepseek1 <Deepseek1@users.noreply.github.com>
2025-11-30 16:47:14 +01:00
Nico
9a9991eb9b
restore as a page (#87)
* feat: add custom restore target directory

Adds the ability to restore snapshots to a custom directory instead of
only the original path. Closes #12.

Changes:
- Add target parameter to restore API endpoint
- Add directory picker UI in file browser restore dialog
- Add target input field in snapshot restore form
- Create reusable PathSelector component

Note: Run `bun run gen:api-client` after merging to regenerate types.

* refactor: path selector design

* refactor: unify restore snapshot dialogs

* refactor: restore snapshot as a page

* chore: fix liniting issues

* chore(create-notification): remove un-used prop

---------

Co-authored-by: Deepseek1 <Deepseek1@users.noreply.github.com>
2025-11-30 16:43:34 +01:00
Jakub Trávník
03b898f84c
Update app/client/modules/notifications/components/create-notification-form.tsx
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-30 15:38:45 +01:00
Jakub Trávník
6fbb11fefe telegram notification 2025-11-30 15:15:26 +01:00
Nico
3bf3b22b96
feat: restore to custom location (#78)
* feat: restore to custom location

* refactor: define overwrite mode in shared schema
2025-11-29 16:53:44 +01:00
Nicolas Meienberger
58708cf35d refactor: repo healthcheck once per day
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2025-11-29 12:25:46 +01:00
Nicolas Meienberger
1d4e7100ab fix: healtcheck, to not read full data 2025-11-29 12:24:07 +01:00
Nicolas Meienberger
0dfe000148 feat: rename volumes & repositories
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2025-11-28 20:47:27 +01:00
Nicolas Meienberger
7d9d3d5d3d fix: wrong compression modes used
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2025-11-28 20:28:47 +01:00
Nicolas Meienberger
8e90c4ace1 refactor: native repository healthcheck 2025-11-28 08:20:06 +01:00
Nicolas Meienberger
803eb1cd76 chore: update favicon 2025-11-28 08:18:34 +01:00
Nico
673827f9f3
refactor: all timestamps to ms (#77)
* refactor: change all timestamps to be in miliseconds

* chore: format files

* chore: fix syntax error
2025-11-26 23:20:22 +01:00
Nicolas Meienberger
4328607cc1 fix: skip renaming imported repository
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2025-11-26 22:20:42 +01:00
Nicolas Meienberger
bedd325a60 fix(db): set pragma after migrations
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2025-11-26 20:12:12 +01:00
Nico
b26a062648
refactor: use short ids to allow changing the name of volumes & repos (#67)
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
* refactor: use short ids to allow changing the name of volumes & repos

* refactor: address PR feedbacks

* fix: make short_id non null after initial population
2025-11-26 19:47:09 +01:00
Nico
d190d9c8cd
feat: partial success warning status (#74)
* feat: report partial backups with warnings

* chore: rebase

* chore: remove un-used size prop
2025-11-26 19:02:29 +01:00
Nicolas Meienberger
f8363a6c71 docs: update readme with TZ environment 2025-11-25 18:25:22 +01:00
Nico
59b2b53837
docs: update README
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2025-11-23 21:21:52 +01:00
Nicolas Meienberger
e99487eed9 fix(notifications): multiple providers using the wrong params
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2025-11-23 21:09:23 +01:00
Nicolas Meienberger
8d4e5d2d4e fix(ntfy): wrong usage of token 2025-11-23 20:49:44 +01:00
Nicolas Meienberger
daea3e64e4 fix(smtp-notification): always use smtp://
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2025-11-23 20:37:21 +01:00
Nicolas Meienberger
70df79079f fix(backups): correctly apply repository compression mode
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2025-11-23 17:53:13 +01:00
Nicolas Meienberger
f1096220dd chore: update readme first screenshot 2025-11-23 11:28:27 +01:00
Nicolas Meienberger
2418870284 docs: update readme
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2025-11-22 17:51:58 +01:00
Nicolas Meienberger
43dfe6b190 chore: fix typing issue 2025-11-22 17:50:58 +01:00
Nicolas Meienberger
8c4939af4e feat(notifications): native support for pushover 2025-11-22 17:48:19 +01:00
Nicolas Meienberger
a622b5e689 feat: exclude specific xattr during restore 2025-11-22 17:39:08 +01:00
Nico
6c30e7e357
Feat/notifications alerts (#52)
* feat: notifications backend & creation

* feat: assign notification to backup schedule

* refactor: status dot one component

* chore(notification-details): remove refetchInterval
2025-11-22 14:58:21 +01:00
Nicolas Meienberger
043f73ea87 fix: properly decode path to support all special unicode characters
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2025-11-21 18:25:27 +01:00
Nicolas Meienberger
518700eef6 chore: update readme version
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2025-11-20 22:02:05 +01:00
Nico
a250c442f8
feat: add sftp repositories support (#36) 2025-11-20 20:31:40 +01:00
Nicolas Meienberger
6981600ad7 docs: update readme version
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2025-11-20 19:15:05 +01:00
Nico
cb0d23fd52
refactor: rebrand to zerobyte (#45)
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
2025-11-20 18:59:57 +01:00
Nicolas Meienberger
0e4c302620 refactor: make healthchecks less expensive 2025-11-20 18:50:40 +01:00
Nico
ef87ca816d
docs: add contributing.md and clarify CLA requirements (#41) 2025-11-18 21:43:51 +01:00
931 changed files with 173324 additions and 14143 deletions

View file

@ -1,19 +1,19 @@
*
**
!turbo.json
!bun.lock
!package.json
!.gitignore
!bunfig.toml
!**/package.json
!**/bun.lock
!**/tsconfig.json
!**/vite.config.ts
!**/react-router.config.ts
!**/build.ts
!**/components.json
!src/**
!app/**
!apps/agent/**
!packages/**
!public/**
# License files and attributions
@ -21,3 +21,7 @@
!NOTICES.md
!LICENSES/**
node_modules/**
dist/**
.output/**
app/test/integration/artifacts/**

14
.editorconfig Normal file
View file

@ -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
tab_width = 4
[*.md]
trim_trailing_whitespace = false

9
.env.example Normal file
View file

@ -0,0 +1,9 @@
SERVER_IP=localhost
ZEROBYTE_DATABASE_URL=./data/zerobyte.db
RESTIC_CACHE_DIR=./data/restic/cache
ZEROBYTE_REPOSITORIES_DIR=./data/repositories
ZEROBYTE_VOLUMES_DIR=./data/volumes
APP_SECRET=<openssl rand -hex 32>
BASE_URL=http://*.localhost
TRUST_PROXY=false
PORT=3000

4
.env.test Normal file
View file

@ -0,0 +1,4 @@
ZEROBYTE_DATABASE_URL=:memory:
APP_SECRET=8b9acd4456dd5db0a4a3c4f4e1240b2c3ae08bb59690167197425e4a25dd9a69
BASE_URL=http://localhost:4096
ENABLE_LOCAL_AGENT=true

13
.fallowrc.json Normal file
View file

@ -0,0 +1,13 @@
{
"entry": [
"app/client.tsx",
"app/router.tsx",
"app/server.ts",
"app/server/plugins/bootstrap.ts",
"openapi-ts.config.ts",
"scripts/create-test-files.ts",
"scripts/patch-api-client.ts"
],
"ignorePatterns": ["node_modules/**", ".output/**", "**/api-client/**", "**/routeTree.gen.ts", "apps/docs/**"],
"ignoreDependencies": ["@tailwindcss/typography", "babel-plugin-react-compiler"]
}

31
.gitattributes vendored Normal file
View file

@ -0,0 +1,31 @@
# 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
Dockerfile* text eol=lf
.dockerignore text eol=lf
docker-compose*.yml 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

View file

@ -0,0 +1,37 @@
name: Feature request
description: Suggest an enhancement or new feature
title: "[FEATURE] "
labels: ["enhancement"]
assignees: []
body:
- type: textarea
id: summary
attributes:
label: Summary
description: One-line summary of the requested feature
placeholder: Brief description of what you'd like to see added
validations:
required: true
- type: textarea
id: motivation
attributes:
label: Motivation / Use case
description: Why is this feature needed? What problem does it solve?
placeholder: Describe the problem or use case that this feature would address
validations:
required: true
- type: textarea
id: proposal
attributes:
label: Proposed solution
description: Brief implementation idea or API surface
placeholder: How do you envision this feature working?
- type: dropdown
id: impact
attributes:
label: Impact
options:
- Low
- Medium
- High
description: Estimated impact on users or project

79
.github/ISSUE_TEMPLATE/2_bug_report.yml vendored Normal file
View file

@ -0,0 +1,79 @@
name: Bug report
description: Create a report to help us fix a bug
title: "[BUG] "
labels: ["bug"]
assignees: []
body:
- type: checkboxes
id: troubleshooting
attributes:
label: Prerequisites
description: Please confirm you have completed the following steps before submitting this bug report
options:
- label: I have read the [troubleshooting guide](https://github.com/nicotsx/zerobyte/blob/main/TROUBLESHOOTING.md)
required: true
- type: checkboxes
id: rclone_preflight
attributes:
label: Rclone Pre-flight Checklist (if applicable)
description: If your issue involves rclone, confirm you've tested on the host first as described in the troubleshooting guide
options:
- label: This issue is NOT related to rclone (skip if not using rclone)
- label: I have tested `rclone listremotes` and `rclone lsd remote:` on the **host** and they work
- label: I have verified the rclone config is mounted into the container
- label: I have restarted the container after config changes
- type: textarea
id: description
attributes:
label: Bug Description
description: Brief description of what went wrong
placeholder: Describe the issue...
validations:
required: true
- type: textarea
id: steps
attributes:
label: Steps to Reproduce
description: List the steps to reproduce the bug
placeholder: |
1. Go to...
2. Click on...
3. See error...
validations:
required: true
- type: textarea
id: expected
attributes:
label: Expected Behavior
description: What should have happened?
validations:
required: true
- type: input
id: version
attributes:
label: Zerobyte version / commit
placeholder: e.g., v1.2.3 or commit sha
validations:
required: true
- type: dropdown
id: deployment
attributes:
label: Deployment Method
options:
- Docker Compose
- Docker
- Local Development
- Other
- type: textarea
id: backup_context
attributes:
label: Backup/Repository Context
description: If this is related to backup operations, provide details about the repository type, storage backend, etc.
placeholder: e.g., S3 repository, local filesystem, specific backup operation that failed
- type: textarea
id: logs
attributes:
label: Logs / Error Messages
description: Check browser console (F12) or server logs for errors. Remove any sensitive information.
validations:
required: true

20
.github/ISSUE_TEMPLATE/3_question.yml vendored Normal file
View file

@ -0,0 +1,20 @@
name: Question
description: General questions about usage or implementation
title: "[QUESTION] "
labels: ["question"]
assignees: []
body:
- type: textarea
id: question
attributes:
label: Question
description: Provide a clear question and any minimal code samples
placeholder: What would you like to know about Zerobyte?
validations:
required: true
- type: textarea
id: context
attributes:
label: Context / what you've tried
description: Provide context about what you're trying to accomplish and what you've already tried
placeholder: I'm trying to... and I've already tried...

21
.github/ISSUE_TEMPLATE/4_security.yml vendored Normal file
View file

@ -0,0 +1,21 @@
name: Security
description: Report a security vulnerability (do not file public issues)
title: "[SECURITY] "
labels: ["security"]
assignees: []
body:
- type: markdown
attributes:
value: >-
**⚠️ SECURITY VULNERABILITY NOTICE ⚠️**
If you believe you've found a security vulnerability, **do not open a public issue**.
Please report security vulnerabilities privately through one of these methods:
- Use GitHub's private security advisory feature: [Report a vulnerability](https://github.com/nicotsx/zerobyte/security/advisories/new)
- Contact the maintainers directly via email if listed in the repository
This helps us address security issues responsibly before they can be exploited.
**Thank you for helping keep Zerobyte secure! 🔒**

19
.github/ISSUE_TEMPLATE/5_blank.yml vendored Normal file
View file

@ -0,0 +1,19 @@
name: Blank Issue
description: Create a blank issue for anything that doesn't fit other templates
title: ""
labels: []
assignees: []
body:
- type: textarea
id: content
attributes:
label: Issue Details
description: Describe your issue, question, or request
placeholder: |
Please provide as much detail as possible about your issue.
If this is a bug, consider using the Bug Report template instead.
If this is a feature request, consider using the Feature Request template instead.
If you have a question, consider using the Question template instead.
validations:
required: true

View file

@ -0,0 +1,31 @@
name: Install dependencies
description: Install dependencies
inputs:
workingDirectory:
description: "The working directory to install dependencies in"
required: false
default: "."
runs:
using: "composite"
steps:
- uses: socketdev/action@937f824ec476dfd164d4a4d9995751427b0be143 # v1
with:
mode: firewall
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
name: Install Bun
with:
bun-version: "1.3.14"
- uses: voidzero-dev/setup-vp@ca1c46663915d6c1042ae23bd39ab85718bfb0fa # v1
with:
node-version: '24'
cache: true
- name: Install dependencies
shell: bash
run: |
cd ${{ inputs.workingDirectory }}
sfw vp install --frozen-lockfile

1
.github/copilot-instructions.md vendored Normal file
View file

@ -0,0 +1 @@
- This project uses the AGENTS.md file to give detailed information about the repository structure and development commands. Make sure to read this file before starting development.

19
.github/renovate.json vendored Normal file
View file

@ -0,0 +1,19 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:recommended", "helpers:pinGitHubActionDigests"],
"enabledManagers": ["bun", "github-actions"],
"minimumReleaseAge": "3 days",
"rangeStrategy": "bump",
"packageRules": [
{
"matchManagers": ["github-actions"],
"matchUpdateTypes": ["digest", "pinDigest"],
"minimumReleaseAge": null
},
{
"matchManagers": ["bun"],
"matchUpdateTypes": ["minor", "patch"],
"groupName": "bun minor and patch dependencies"
}
]
}

96
.github/workflows/checks.yml vendored Normal file
View file

@ -0,0 +1,96 @@
name: Checks
on:
workflow_call:
pull_request:
push:
branches:
- main
permissions:
contents: read
concurrency:
group: checks-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
lint:
timeout-minutes: 10
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 1
- name: Install dependencies
uses: "./.github/actions/install-dependencies"
- name: Run lint
shell: bash
run: vp lint --type-aware --deny-warnings
typecheck:
timeout-minutes: 10
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 1
- name: Install dependencies
uses: "./.github/actions/install-dependencies"
- name: Run type checks
shell: bash
run: bun run tsc
test-server:
timeout-minutes: 15
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 1
- name: Install dependencies
uses: "./.github/actions/install-dependencies"
- name: Run server tests
shell: bash
run: bun run test:server
test-client:
timeout-minutes: 15
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 1
- name: Install dependencies
uses: "./.github/actions/install-dependencies"
- name: Run client tests
shell: bash
run: bun run test:client
build:
timeout-minutes: 15
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 1
- name: Install dependencies
uses: "./.github/actions/install-dependencies"
- name: Build project
shell: bash
run: bun run build

41
.github/workflows/docs-deploy.yml vendored Normal file
View file

@ -0,0 +1,41 @@
name: Docs deploy
permissions:
contents: read
on:
workflow_dispatch:
push:
branches:
- main
paths:
- "apps/docs/**"
jobs:
deploy:
name: Deploy docs
if: github.event.repository.default_branch == github.ref_name
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Install dependencies
uses: "./.github/actions/install-dependencies"
- name: Build docs
working-directory: apps/docs
run: bun run build
- name: Build and deploy docs
uses: cloudflare/wrangler-action@ebbaa1584979971c8614a24965b4405ff95890e0 # v4
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
workingDirectory: apps/docs

108
.github/workflows/e2e.yml vendored Normal file
View file

@ -0,0 +1,108 @@
name: E2E Tests
permissions:
contents: read
on:
workflow_dispatch:
workflow_call:
jobs:
playwright:
name: Playwright
timeout-minutes: 20
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Install dependencies
uses: "./.github/actions/install-dependencies"
- name: Get Playwright version
id: playwright-version
shell: bash
run: |
playwright_version=$(bun -e "console.log((await Bun.file('./package.json').json()).devDependencies['@playwright/test'])")
echo "version=$playwright_version" >> $GITHUB_OUTPUT
- name: Cache Playwright Browsers
id: playwright-cache
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: ~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ steps.playwright-version.outputs.version }}
restore-keys: |
${{ runner.os }}-playwright-
- name: Install Playwright Browsers
if: steps.playwright-cache.outputs.cache-hit != 'true'
run: bunx playwright install --with-deps
- name: Install Playwright System Dependencies
if: steps.playwright-cache.outputs.cache-hit == 'true'
run: bunx playwright install-deps
- name: Prepare environment
run: |
mkdir -p data
mkdir -p playwright/temp
mkdir -p playwright/data
mkdir -p playwright/tinyauth/app-data
mkdir -p playwright/tinyauth/caddy-data
chmod -R 777 playwright/temp
touch .env.local
echo "SERVER_IP=localhost" >> .env.local
echo "ZEROBYTE_DATABASE_URL=./data/zerobyte.db" >> .env.local
echo "BASE_URL=http://localhost:4096" >> .env.local
echo "E2E_TINYAUTH_ORIGIN=https://tinyauth.example.com:5557" >> .env.local
echo "TZ=Europe/Zurich" >> .env.local
- name: Start zerobyte-e2e service
run: bun run start:e2e -- -d
- name: Check CLI version
run: |
expected_version=$(docker exec zerobyte printenv APP_VERSION)
cli_version=$(docker exec zerobyte bun run --silent cli --version)
echo "$cli_version"
test "$cli_version" = "$expected_version"
- name: Wait for zerobyte to be ready
run: |
timeout 30s bash -c 'until curl -f http://localhost:4096/api/healthcheck; do echo "Waiting for server..." && sleep 2; done'
- name: Wait for Tinyauth to be ready
run: |
timeout 30s bash -c 'until curl -skf https://localhost:5557/.well-known/openid-configuration; do echo "Waiting for Tinyauth..." && sleep 2; done'
- name: Print docker logs if failed to start
if: failure()
run: docker compose logs zerobyte-e2e tinyauth tinyauth-app || true
- name: Make playwright directory writable
run: sudo chmod 777 playwright/data/zerobyte.db
- name: Run Playwright tests
run: bun run test:e2e
- name: Dump container logs in playwright-report folder
if: always()
run: |
tree playwright || true
docker compose logs zerobyte-e2e > playwright-report/container-logs.txt || true
docker compose logs tinyauth tinyauth-app > playwright-report/tinyauth-logs.txt || true
- name: Debug - print content of /test-data in container
if: failure()
run: |
docker exec zerobyte /bin/ash -c "ls -la /test-data" || true
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 5
- name: Stop Docker Compose
if: always()
run: docker compose down

35
.github/workflows/integration.yml vendored Normal file
View file

@ -0,0 +1,35 @@
name: Integration Tests
permissions:
contents: read
on:
workflow_dispatch:
workflow_call:
jobs:
integration:
name: Integration
timeout-minutes: 30
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Install dependencies
uses: "./.github/actions/install-dependencies"
- name: Run integration tests
env:
SKIP_VOLUME_MOUNT_INTEGRATION_TESTS: "true"
run: bun run test:integration
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
if: failure()
with:
name: integration-artifacts
path: |
app/test/integration/artifacts/compose.log
app/test/integration/artifacts/docker-output.log
app/test/integration/artifacts/runs/**
retention-days: 5

60
.github/workflows/nightly.yml vendored Normal file
View file

@ -0,0 +1,60 @@
name: Nightly Build
on:
schedule:
- cron: "0 2 * * *"
workflow_dispatch:
permissions:
contents: read
jobs:
build-and-push:
timeout-minutes: 15
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Log in to Docker Hub
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4
with:
driver: cloud
endpoint: "meienberger/runtipi-builder"
install: true
- name: Login to GitHub Container Registry
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Docker meta
id: meta
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6
with:
images: ghcr.io/${{ github.repository_owner }}/zerobyte
tags: |
type=raw,value=nightly
- name: Push images to GitHub Container Registry
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7
with:
context: .
target: production
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
APP_VERSION=nightly

View file

@ -7,12 +7,17 @@ on:
- "v*.*.*-beta.*"
- "v*.*.*-alpha.*"
permissions:
contents: read
jobs:
determine-release-type:
runs-on: ubuntu-latest
permissions: {}
outputs:
release_type: ${{ steps.set-type.outputs.release_type }}
tagname: ${{ github.ref_name }}
is_minor_release: ${{ steps.set-type.outputs.is_minor_release }}
steps:
- name: Set release type
id: set-type
@ -26,25 +31,52 @@ jobs:
echo "release_type=release" >> $GITHUB_OUTPUT
fi
if [[ $TAG =~ ^v[0-9]+\.[0-9]+\.0$ ]]; then
echo "is_minor_release=true" >> $GITHUB_OUTPUT
else
echo "is_minor_release=false" >> $GITHUB_OUTPUT
fi
checks:
uses: ./.github/workflows/checks.yml
e2e-tests:
uses: ./.github/workflows/e2e.yml
integration-tests:
uses: ./.github/workflows/integration.yml
build-images:
environment: release
timeout-minutes: 15
needs: [determine-release-type]
needs: [determine-release-type, checks, e2e-tests, integration-tests]
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
security-events: write
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
ref: ${{ github.ref }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Log in to Docker Hub
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4
with:
driver: cloud
endpoint: "meienberger/runtipi-builder"
install: true
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@ -52,9 +84,9 @@ jobs:
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6
with:
images: ghcr.io/${{ github.repository_owner }}/ironmount
images: ghcr.io/${{ github.repository_owner }}/zerobyte
tags: |
type=semver,pattern={{version}},prefix=v
type=semver,pattern={{major}},prefix=v,enable=${{ needs.determine-release-type.outputs.release_type == 'release' }}
@ -62,11 +94,9 @@ jobs:
type=semver,pattern={{major}}.{{minor}}.{{patch}},prefix=v,enable=${{ needs.determine-release-type.outputs.release_type == 'release' }}
flavor: |
latest=${{ needs.determine-release-type.outputs.release_type == 'release' }}
cache-from: type=registry,ref=ghcr.io/nicotsx/ironmount:buildcache
cache-to: type=registry,ref=ghcr.io/nicotsx/ironmount:buildcache,mode=max
- name: Build and push images
uses: docker/build-push-action@v6
- name: Push images to GitHub Container Registry
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7
with:
context: .
target: production
@ -81,12 +111,14 @@ jobs:
runs-on: ubuntu-latest
needs: [build-images, determine-release-type]
if: needs.determine-release-type.outputs.release_type == 'release'
permissions:
contents: write
outputs:
id: ${{ steps.create_release.outputs.id }}
steps:
- name: Create GitHub release
id: create_release
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
@ -96,4 +128,13 @@ jobs:
name: ${{ needs.determine-release-type.outputs.tagname }}
draft: false
prerelease: true
files: cli/runtipi-cli-*
request-docs-version-update:
uses: ./.github/workflows/request-docs-version-update.yml
needs: [publish-release, determine-release-type]
if: needs.determine-release-type.outputs.release_type == 'release' && needs.determine-release-type.outputs.is_minor_release == 'true'
with:
release_tag: ${{ needs.determine-release-type.outputs.tagname }}
base_branch: ${{ github.event.repository.default_branch }}
secrets:
COPILOT_ASSIGNMENT_TOKEN: ${{ secrets.COPILOT_ASSIGNMENT_TOKEN }}

View file

@ -0,0 +1,122 @@
name: Request docs version update
on:
workflow_dispatch:
inputs:
release_tag:
description: Minor release tag, for example v0.35.0
required: true
type: string
base_branch:
description: Branch Copilot should branch from
required: false
default: main
type: string
workflow_call:
inputs:
release_tag:
required: true
type: string
base_branch:
required: false
default: main
type: string
secrets:
COPILOT_ASSIGNMENT_TOKEN:
required: true
permissions:
contents: read
jobs:
request-docs-version-update:
runs-on: ubuntu-latest
steps:
- name: Create Copilot issue
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
github-token: ${{ secrets.COPILOT_ASSIGNMENT_TOKEN }}
script: |
if (!process.env.COPILOT_ASSIGNMENT_TOKEN) {
core.setFailed("COPILOT_ASSIGNMENT_TOKEN is required to assign issues to GitHub Copilot.");
return;
}
const releaseTag = process.env.RELEASE_TAG;
const baseBranch = process.env.BASE_BRANCH;
const minorRelease = releaseTag.match(/^(v\d+\.\d+)\.0$/);
if (!minorRelease) {
core.setFailed(`Docs version updates only run for minor release tags like v0.35.0. Received: ${releaseTag}`);
return;
}
const docsVersion = minorRelease[1];
const { owner, repo } = context.repo;
const targetRepo = `${owner}/${repo}`;
const releaseUrl = `https://github.com/${targetRepo}/releases/tag/${releaseTag}`;
const title = `Update Docker Compose examples to ${docsVersion}`;
const bodyLines = [
`A new Zerobyte release was published: ${releaseTag}`,
"",
`Release: ${releaseUrl}`,
"",
"Please create a pull request that updates Docker Compose documentation examples to use:",
"",
"```yaml",
`image: ghcr.io/nicotsx/zerobyte:${docsVersion}`,
"```",
"",
"Scope:",
"",
"- Update Docker image tags in `README.md`.",
"- Update Docker image tags in `apps/docs/`.",
"- Only update Zerobyte image versions in Docker Compose examples.",
"- Do not change unrelated prose, formatting, or code.",
"",
"Verification:",
"",
"- Search for stale `ghcr.io/nicotsx/zerobyte:v...` references in `README.md` and `apps/docs/`.",
"- Run any formatting or documentation checks that are appropriate for the touched files.",
];
const issue = await github.rest.issues.create({
owner,
repo,
title,
body: bodyLines.join("\n"),
});
await github.rest.issues.update({
owner,
repo,
issue_number: issue.data.number,
body: [
...bodyLines,
"",
"Pull request requirement:",
"",
`- Include \`Closes #${issue.data.number}\` in the pull request body.`,
].join("\n"),
});
await github.request("POST /repos/{owner}/{repo}/issues/{issue_number}/assignees", {
owner,
repo,
issue_number: issue.data.number,
assignees: ["copilot-swe-agent[bot]"],
agent_assignment: {
target_repo: targetRepo,
base_branch: baseBranch,
custom_instructions: "Create a focused documentation-only PR for the release version update.",
custom_agent: "",
model: "",
},
headers: {
accept: "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
},
});
env:
RELEASE_TAG: ${{ inputs.release_tag }}
BASE_BRANCH: ${{ inputs.base_branch }}
COPILOT_ASSIGNMENT_TOKEN: ${{ secrets.COPILOT_ASSIGNMENT_TOKEN }}

41
.github/workflows/scorecard.yml vendored Normal file
View file

@ -0,0 +1,41 @@
name: Scorecard analysis
on:
branch_protection_rule:
push:
branches: [ "main" ]
permissions: read-all
jobs:
analysis:
name: Scorecard analysis
runs-on: ubuntu-latest
if: github.event.repository.default_branch == github.ref_name || github.event_name == 'pull_request'
permissions:
security-events: write
id-token: write
steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
persist-credentials: false
- name: Run analysis
uses: ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186
with:
results_file: results.sarif
results_format: sarif
publish_results: true
- name: Upload artifact
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1
with:
name: SARIF file
path: results.sarif
retention-days: 5
- name: Upload to code-scanning
uses: github/codeql-action/upload-sarif@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4
with:
sarif_file: results.sarif

38
.gitignore vendored
View file

@ -12,3 +12,41 @@ CLAUDE.md
mutagen.yml.lock
notes.md
smb-password.txt
cache.db
/data
.env*
!.env.example
!.env.test
# Playwright
node_modules/
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
/playwright/.auth/
/playwright/*.pass
/playwright/tinyauth/app-data/
/playwright/tinyauth/caddy-data/
playwright/.auth
playwright/temp
playwright/data
.idea/
.deepsec/
.codex/
# OpenAPI error logs
openapi-ts-error-*.log
.output
tmp/
qa-output
.worktrees/
.turbo
.superset
.fallow

1
.vite-hooks/pre-commit Executable file
View file

@ -0,0 +1 @@
vp staged

3
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,3 @@
{
"editor.defaultFormatter": "oxc.oxc-vscode"
}

82
AGENTS.md Normal file
View file

@ -0,0 +1,82 @@
## Important instructions
- Never create migration files manually. Always use the provided command to generate migrations
- If you realize an automated migration is incorrect, make sure to remove all the associated entries from the `_journal.json` and the newly created files located in `app/drizzle/` before re-generating the migration
- The dev server runs through Portless. Start it with `bun run dev`, then use `portless get zerobyte` to get the current worktree-specific URL. Do not assume a fixed port like `3000` or `4096`. Username is `admin` and password is `password`
- The repo is https://github.com/nicotsx/zerobyte
- If you need to run a specific restic command on a repository, you can open and use the dev panel with `Meta+Shift+D`
## Project Overview
Zerobyte is a backup automation tool built on top of Restic that provides a web interface for scheduling, managing, and monitoring encrypted backups. It supports multiple volume backends (NFS, SMB, WebDAV, SFTP, local directories) and repository backends (S3, Azure, GCS, local, and rclone-based storage).
### Development Server
```bash
# Start the dev server through Portless
bun run dev
# Get the current app URL for this worktree
portless get zerobyte
# Inspect active Portless routes if needed
portless list
```
Portless applies git worktree prefixes automatically, so linked worktrees may return URLs like `https://branch-name.zerobyte.localhost`. Use the Portless URL for browser testing and manual verification.
### Type Checking
```bash
# Run type checking and generate React Router types
bun run tsc
```
### Testing
```bash
# Run all tests
bun run test
# Run a specific test file
bunx dotenv-cli -e .env.test -- bunx --bun vitest run --project server path/to/test.ts
```
### Building
```bash
# Build for production
bun run build
```
### Database Migrations
```bash
# Generate new migration from schema changes
bun gen:migrations
# Generate a custom empty migration
bunx drizzle-kit generate --custom --name=fix-timestamps-to-ms
```
### API Client Generation
```bash
bun run gen:api-client
```
### Code Quality
```bash
# Format
vp fmt <path> --write
# Lint
bun run lint
```
### Invalidation
The frontend has an automatic invalidation setup which runs after every mutation.
Do not implement any invalidation logic in the frontend.

167
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,167 @@
# Contributing to Zerobyte
Thank you for your interest in contributing to Zerobyte! We welcome contributions from the community and are grateful for your support in making this project better.
## Table of Contents
- [Getting Started](#getting-started)
- [Contributor License Agreement (CLA)](#contributor-license-agreement-cla)
- [How to Contribute](#how-to-contribute)
- [Development Setup](#development-setup)
- [Submission Guidelines](#submission-guidelines)
- [Code Standards](#code-standards)
- [Community Guidelines](#community-guidelines)
## Getting Started
Before you begin:
1. Check the [issues](https://github.com/nicotsx/zerobyte/issues) to see if someone is already working on what you have in mind
2. For major changes, please open an issue first to discuss what you would like to change
3. Make sure you have read and agreed to our Contributor License Agreement (CLA)
## Contributor License Agreement (CLA)
### What is a CLA?
A Contributor License Agreement (CLA) is a legal document in which you state you are entitled to contribute the code/documentation/translation to the project you're contributing to and are willing to have it used in distributions and derivative works. This means you grant us permission to use your contributions under our project's license terms.
### Why do we need a CLA?
We require a CLA for several important reasons:
1. **License Flexibility**: It allows the project to evolve its licensing model if needed in the future without requiring re-approval from all past contributors, ensuring Zerobyte can continue to operate and adapt to changing needs of the community.
2. **Patent Protection**: The CLA includes a patent license grant, which protects the project and its users from potential patent claims related to your contributions.
3. **Protecting Your Rights**: While you grant us rights to use your contributions, you retain ownership of your work and can use it for any other purpose.
### How to Sign the CLA
When you submit your first pull request, our CLA Assistant will automatically prompt you to sign the agreement via GitHub. The process is simple:
1. Create your pull request
2. The CLA Assistant bot will comment on your PR
3. Follow the link provided to review and sign the CLA electronically
4. Once signed, the bot will update your PR status
You only need to sign the CLA once, and it will cover all your future contributions to Zerobyte.
### Key Points of Our CLA
- You grant us a non-exclusive, royalty-free license to use your contributions
- You retain ownership and all rights to your contributions
- You confirm that you have the right to make the contribution (it's your original work or you have permission)
- You're not required to provide support for your contributions
- The CLA does not guarantee that your contribution will be accepted or kept into the project
For the complete CLA text, please see the [CLA document](https://cla-assistant.io/nicotsx/zerobyte).
## How to Contribute
There are many ways to contribute to Zerobyte:
### Reporting Bugs
If you find a bug, please open an issue with:
- A clear, descriptive title
- Steps to reproduce the issue
- Expected vs. actual behavior
- Your environment (OS, Docker version, Zerobyte version)
- Any relevant logs or screenshots
### Suggesting Features
When suggesting a feature:
- Check if it's already been suggested
- Clearly describe the feature and its use case
- Explain why it would be valuable to other users
- Consider the scope and complexity
### Contributing Code
1. **Fork the repository** and create your branch from `main`
2. **Make your changes** following our code standards
3. **Test your changes** thoroughly
4. **Update documentation** if needed
5. **Commit your changes** with clear, descriptive commit messages
6. **Push to your fork** and submit a pull request
### Improving Documentation
Documentation improvements are always welcome! This includes:
- Fixing typos or clarifying existing docs
- Adding examples or use cases
- Writing guides or tutorials
- Improving README or other documentation files
### Translations
We welcome translations to make Zerobyte accessible to more users worldwide. Please open an issue to discuss translation efforts before starting.
## Development Setup
1. **Clone your fork**:
```bash
git clone https://github.com/your-username/zerobyte.git
cd zerobyte
```
2. **Set up your development environment**:
```bash
bun run start:dev
```
3. **Create a feature branch**:
```bash
git checkout -b feature/your-feature-name
```
4. **Make your changes and test them**
5. **Commit your changes**:
```bash
git add .
git commit -m "Add your descriptive commit message"
```
## Submission Guidelines
### Pull Request Process
1. **Update your branch** with the latest changes from main before submitting
2. **Ensure all tests pass** and your code builds successfully
3. **Write a clear PR description** that explains:
- What changes you made
- Why you made them
- Any breaking changes or migration notes
- Link to related issues
4. **Be responsive** to feedback and review comments
5. **Keep PRs focused** - one feature or fix per PR when possible
## Code Standards
- Follow the existing code style and conventions
- Write clear, self-documenting code. No unless comments are necessary
- Ensure your code is properly formatted
- Keep security in mind - never commit sensitive data like passwords or API keys
## Community Guidelines
- Be respectful and constructive in all interactions
- Welcome newcomers and help them get started
- Assume good intentions
- Focus on what is best for the community and the project
- Show empathy towards other community members
## Questions?
If you have questions about contributing, feel free to:
- Open an issue with your question
- Check existing issues and discussions
- Reach out to the maintainers
---
Thank you for contributing to Zerobyte!

View file

@ -1,8 +1,18 @@
ARG BUN_VERSION="1.3.1"
FROM oven/bun:1.3.14-alpine@sha256:5acc90a93e91ff07bf72aa90a7c9f0fa189765aec90b47bdbf2152d2196383c0 AS base
FROM oven/bun:${BUN_VERSION}-alpine AS base
ARG RESTIC_VERSION="0.18.1"
ARG RCLONE_VERSION="1.74.2"
ARG SHOUTRRR_VERSION="0.15.1"
RUN apk add --no-cache davfs2=1.6.1-r2
ENV VITE_RESTIC_VERSION=${RESTIC_VERSION} \
VITE_RCLONE_VERSION=${RCLONE_VERSION} \
VITE_SHOUTRRR_VERSION=${SHOUTRRR_VERSION}
RUN apk update --no-cache && \
apk upgrade --no-cache && \
apk add --no-cache acl attr cifs-utils davfs2=1.6.1-r2 openssh-client fuse3 sshfs tini tzdata
ENTRYPOINT ["/sbin/tini", "-s", "--"]
# ------------------------------
@ -13,79 +23,105 @@ FROM base AS deps
WORKDIR /deps
ARG TARGETARCH
ARG RESTIC_VERSION="0.18.1"
ENV TARGETARCH=${TARGETARCH}
RUN apk add --no-cache curl bzip2
RUN apk add --no-cache curl bzip2 unzip tar
RUN echo "Building for ${TARGETARCH}"
RUN if [ "${TARGETARCH}" = "arm64" ]; then \
curl -L -o restic.bz2 "https://github.com/restic/restic/releases/download/v$RESTIC_VERSION/restic_$RESTIC_VERSION"_linux_arm64.bz2; \
curl -O https://downloads.rclone.org/rclone-current-linux-arm64.zip; \
unzip rclone-current-linux-arm64.zip; \
curl -fL -o restic.bz2 "https://github.com/restic/restic/releases/download/v${RESTIC_VERSION}/restic_${RESTIC_VERSION}_linux_arm64.bz2"; \
curl -fL -o rclone.zip "https://github.com/rclone/rclone/releases/download/v${RCLONE_VERSION}/rclone-v${RCLONE_VERSION}-linux-arm64.zip"; \
unzip rclone.zip; \
curl -fL -o shoutrrr.tar.gz "https://github.com/nicholas-fedor/shoutrrr/releases/download/v${SHOUTRRR_VERSION}/shoutrrr_linux_arm64v8_${SHOUTRRR_VERSION}.tar.gz"; \
elif [ "${TARGETARCH}" = "amd64" ]; then \
curl -L -o restic.bz2 "https://github.com/restic/restic/releases/download/v$RESTIC_VERSION/restic_$RESTIC_VERSION"_linux_amd64.bz2; \
curl -O https://downloads.rclone.org/rclone-current-linux-amd64.zip; \
unzip rclone-current-linux-amd64.zip; \
curl -fL -o restic.bz2 "https://github.com/restic/restic/releases/download/v${RESTIC_VERSION}/restic_${RESTIC_VERSION}_linux_amd64.bz2"; \
curl -fL -o rclone.zip "https://github.com/rclone/rclone/releases/download/v${RCLONE_VERSION}/rclone-v${RCLONE_VERSION}-linux-amd64.zip"; \
unzip rclone.zip; \
curl -fL -o shoutrrr.tar.gz "https://github.com/nicholas-fedor/shoutrrr/releases/download/v$SHOUTRRR_VERSION/shoutrrr_linux_amd64_${SHOUTRRR_VERSION}.tar.gz"; \
fi
RUN bzip2 -d restic.bz2 && chmod +x restic
RUN mv rclone-*-linux-*/rclone /deps/rclone && chmod +x /deps/rclone
RUN mv rclone-v*-linux-*/rclone /deps/rclone && chmod +x /deps/rclone
RUN tar -xzf shoutrrr.tar.gz && chmod +x shoutrrr
# ------------------------------
# RUNTIME TOOLS
# ------------------------------
FROM base AS runtime-tools
COPY --from=deps /deps/restic /usr/local/bin/restic
COPY --from=deps /deps/rclone /usr/local/bin/rclone
COPY --from=deps /deps/shoutrrr /usr/local/bin/shoutrrr
# ------------------------------
# DEVELOPMENT
# ------------------------------
FROM base AS development
ARG APP_VERSION=dev
ENV APP_VERSION=${APP_VERSION}
ENV VITE_APP_VERSION=${APP_VERSION}
ENV NODE_ENV="development"
WORKDIR /app
COPY --from=deps /deps/restic /usr/local/bin/restic
COPY --from=deps /deps/rclone /usr/local/bin/rclone
COPY ./package.json ./bun.lock ./
COPY --from=deps /deps/shoutrrr /usr/local/bin/shoutrrr
RUN bun install --frozen-lockfile
COPY ./package.json ./bun.lock ./
COPY ./packages/core/package.json ./packages/core/package.json
COPY ./packages/contracts/package.json ./packages/contracts/package.json
COPY ./apps/agent/package.json ./apps/agent/package.json
COPY ./apps/docs/package.json ./apps/docs/package.json
RUN VITE_GIT_HOOKS=0 bun install --frozen-lockfile --ignore-scripts
COPY . .
EXPOSE 4096
EXPOSE 3000
CMD ["bun", "run", "dev"]
# ------------------------------
# PRODUCTION
# ------------------------------
FROM oven/bun:${BUN_VERSION} AS builder
FROM base AS builder
ARG APP_VERSION=dev
ENV VITE_APP_VERSION=${APP_VERSION}
ENV PORT=4096
WORKDIR /app
COPY ./package.json ./bun.lock ./
RUN bun install --frozen-lockfile
COPY ./packages/core/package.json ./packages/core/package.json
COPY ./packages/contracts/package.json ./packages/contracts/package.json
COPY ./apps/agent/package.json ./apps/agent/package.json
COPY ./apps/docs/package.json ./apps/docs/package.json
RUN VITE_GIT_HOOKS=0 bun install --frozen-lockfile
COPY . .
RUN touch .env
RUN echo "VITE_APP_VERSION=${APP_VERSION}" >> .env
RUN bun run build
RUN bun build apps/agent/src/index.ts --outfile .output/agent/index.mjs --target bun
FROM base AS production
ARG APP_VERSION=dev
ENV APP_VERSION=${APP_VERSION}
ENV NODE_ENV="production"
ENV PORT=4096
WORKDIR /app
COPY --from=builder /app/package.json ./
RUN bun install --production --frozen-lockfile
COPY --from=deps /deps/restic /usr/local/bin/restic
COPY --from=deps /deps/rclone /usr/local/bin/rclone
COPY --from=builder /app/dist/client ./dist/client
COPY --from=builder /app/dist/server ./dist/server
COPY --from=deps /deps/shoutrrr /usr/local/bin/shoutrrr
COPY --from=builder /app/.output ./.output
COPY --from=builder /app/app/drizzle ./assets/migrations
# Include third-party licenses and attribution
@ -95,5 +131,4 @@ COPY ./LICENSE ./LICENSE.md
EXPOSE 4096
CMD ["bun", "run", "start"]
CMD ["bun", ".output/server/index.mjs"]

368
README.md
View file

@ -1,12 +1,12 @@
<div align="center">
<h1>Ironmount</h1>
<h1>Zerobyte</h1>
<h3>Powerful backup automation for your remote storage<br />Encrypt, compress, and protect your data with ease</h3>
<a href="https://github.com/nicotsx/ironmount/blob/main/LICENSE">
<img src="https://img.shields.io/github/license/nicotsx/ironmount" />
<a href="https://github.com/nicotsx/zerobyte/blob/main/LICENSE">
<img src="https://img.shields.io/github/license/nicotsx/zerobyte" />
</a>
<br />
<figure>
<img src="https://github.com/nicotsx/ironmount/blob/main/screenshots/backup-details.png?raw=true" alt="Demo" />
<img src="https://github.com/nicotsx/zerobyte/blob/main/screenshots/backup-details.webp?raw=true" alt="Demo" />
<figcaption>
<p align="center">
Backup management with scheduling and monitoring
@ -15,29 +15,43 @@
</figure>
</div>
#### Join the community
[![Discord](https://img.shields.io/discord/1466834119873925263?label=discord&logo=discord)](https://discord.gg/XjgVyXrcEH)
> [!WARNING]
> Ironmount is still in version 0.x.x and is subject to major changes from version to version. I am developing the core features and collecting feedbacks. Expect bugs! Please open issues or feature requests
> Zerobyte is still in version 0.x.x and is subject to major changes from version to version. I am developing the core features and collecting feedback. Please open issues for bugs or feature requests.
## Intro
<p align="center">
<a href="https://www.buymeacoffee.com/nicotsx" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 60px !important;width: 217px !important;" ></a>
</p>
Ironmount is a backup automation tool that helps you save your data across multiple storage backends. Built on top of Restic, it provides an modern web interface to schedule, manage, and monitor encrypted backups of your remote storage.
## Introduction
Zerobyte is a backup automation tool that helps you save your data across multiple storage backends. Built on top of Restic, it provides an modern web interface to schedule, manage, and monitor encrypted backups of your remote storage.
## Documentation
The official documentation website is available at [zerobyte.app](https://zerobyte.app).
It contains up-to-date setup guides, configuration reference, and usage documentation for running Zerobyte in production.
### Features
- &nbsp; **Automated backups** with encryption, compression and retention policies powered by Restic
- &nbsp; **Flexible scheduling** For automated backup jobs with fine-grained retention policies
- &nbsp; **End-to-end encryption** ensuring your data is always protected
- &nbsp; **Multi-protocol support**: Backup from NFS, SMB, WebDAV, or local directories
- **Automated backups** with encryption, compression, and retention policies, powered by Restic
- **Flexible scheduling** for automated backup jobs with fine-grained retention policies
- **End-to-end encryption** will ensure your data is always protected
- **Multi-protocol support** for backup from NFS, SMB, WebDAV, SFTP, or local directories
## Installation
In order to run Ironmount, you need to have Docker and Docker Compose installed on your server. Then, you can use the provided `docker-compose.yml` file to start the application.
In order to run Zerobyte, you need to have Docker and Docker Compose installed on your server. Then, you can use the provided `docker-compose.yml` file to start the application.
```yaml
services:
ironmount:
image: ghcr.io/nicotsx/ironmount:v0.10
container_name: ironmount
zerobyte:
image: ghcr.io/nicotsx/zerobyte:v0.38
container_name: zerobyte
restart: unless-stopped
cap_add:
- SYS_ADMIN
@ -45,15 +59,33 @@ services:
- "4096:4096"
devices:
- /dev/fuse:/dev/fuse
environment:
- TZ=Europe/Zurich # Set your timezone here
- BASE_URL=http://localhost:4096 # URL you will use to access Zerobyte
- APP_SECRET=94bad46...c66e25d5c2b # Generate your own secret with `openssl rand -hex 32`
volumes:
- /etc/localtime:/etc/localtime:ro
- /var/lib/ironmount:/var/lib/ironmount
- /var/lib/zerobyte:/var/lib/zerobyte
```
> [!WARNING]
> Do not try to change the location of the bind mount `/var/lib/ironmount` on your host or store it on a network share. You will likely face permission issues and strong performance degradation.
> It is highly discouraged to run Zerobyte on a server that is accessible from the internet (VPS or home server with port forwarding). If you do, make sure to change the port mapping to "127.0.0.1:4096:4096" and use a secure tunnel (SSH tunnel, Cloudflare Tunnel, etc.) with authentication.
Then, run the following command to start Ironmount:
> [!WARNING]
> Do not try to point `/var/lib/zerobyte` to a network share. You will face permission issues and strong performance degradation.
> [!NOTE]
> **TrueNAS Users:** The host path `/var/lib` is ephemeral on TrueNAS and will be reset during system upgrades. Instead of using `/var/lib/zerobyte:/var/lib/zerobyte`, create a dedicated ZFS dataset (e.g., `tank/docker/zerobyte`) and mount it instead:
>
> ```yaml
> volumes:
> - /etc/localtime:/etc/localtime:ro
> - /mnt/tank/docker/zerobyte:/var/lib/zerobyte
> ```
>
> This ensures your configuration, encryption keys, and database persist across TrueNAS upgrades.
Then, run the following command to start Zerobyte:
```bash
docker compose up -d
@ -61,19 +93,110 @@ docker compose up -d
Once the container is running, you can access the web interface at `http://<your-server-ip>:4096`.
## Configuration
Zerobyte can be customized using environment variables. Below are the available options:
### Environment variables
| Variable | Description | Default |
| :------------------------ | :---------------------------------------------------------------------------------------------------------------------------------------- | :--------------------- |
| `BASE_URL` | **Required.** The base URL of your Zerobyte instance (e.g., `https://zerobyte.example.com`). See [Authentication](#authentication) below. | (none) |
| `APP_SECRET` | **Required.** A random secret key (32+ chars) used to encrypt sensitive data in the database. Generate with `openssl rand -hex 32`. | (none) |
| `APP_SECRET_FILE` | Path to a file containing `APP_SECRET`, useful with Docker or Kubernetes secrets. Mutually exclusive with `APP_SECRET`. | (none) |
| `PORT` | The port the web interface and API will listen on. | `4096` |
| `RESTIC_HOSTNAME` | The hostname used by Restic when creating snapshots. Automatically detected if a custom hostname is set in Docker. | `zerobyte` |
| `TZ` | Timezone for the container (e.g., `Europe/Zurich`). **Crucial for accurate backup scheduling.** | `UTC` |
| `TRUST_PROXY` | When `true`, trust an existing `X-Forwarded-For` header from your reverse proxy. Leave `false` for direct deployments. | `false` |
| `TRUSTED_ORIGINS` | Comma-separated list of extra trusted origins for CORS (e.g., `http://localhost:3000,http://example.com`). | (none) |
| `WEBHOOK_ALLOWED_ORIGINS` | Comma-separated list of HTTP origins allowed for backup webhooks and outbound HTTP notification destinations. | (none) |
| `WEBHOOK_TIMEOUT` | Timeout for backup webhook requests in seconds. | `60` |
| `LOG_LEVEL` | Logging verbosity. Options: `debug`, `info`, `warn`, `error`. | `info` |
| `SERVER_IDLE_TIMEOUT` | Idle timeout for the server in seconds. | `60` |
| `RCLONE_CONFIG_DIR` | Path to the directory containing `rclone.conf` inside the container. Change this if running as a non-root user. | `/root/.config/rclone` |
| `PROVISIONING_PATH` | Path to a JSON file with operator-managed repositories and volumes to sync at startup. | (none) |
### Webhook and notification network policy
Backup webhooks and outbound notification destinations that can target arbitrary network hosts are restricted by `WEBHOOK_ALLOWED_ORIGINS`.
The allowlist matches exact origins only: scheme, host, and port must match. Paths are ignored, so `https://hooks.example.com/backups` allows any path on `https://hooks.example.com`, but it does not allow `http://hooks.example.com`, `https://hooks.example.com:8443`, or `https://other.example.com`.
This policy applies to:
- backup pre/post webhook URLs
- Generic HTTP notification URLs
- Gotify server URLs
- self-hosted ntfy server URLs
- custom Shoutrrr URLs that point at generic HTTP or SMTP network targets
The public ntfy.sh service and fixed-provider notification services such as Slack, Discord, Pushover, and Telegram do not need `WEBHOOK_ALLOWED_ORIGINS`.
Backup webhooks do not follow redirects. Add the final destination origin to `WEBHOOK_ALLOWED_ORIGINS` and configure that final URL directly.
Webhook headers are stored as plain text and must use one `Key: Value` header per line. `WEBHOOK_TIMEOUT` controls backup pre/post webhook request timeouts; notification delivery uses the underlying provider sender behavior.
### Provisioned resources
Zerobyte can sync operator-managed repositories and volumes from a JSON file at startup. This is useful when you want credentials or connection details to live in deployment-time configuration instead of being entered through the UI.
Provisioned resources:
- appear in the normal repositories and volumes screens
- can resolve credential fields from environment variables or `/run/secrets/*` during startup sync
The complete provisioning documentation is available at [zerobyte.app/docs/guides/provisioning](https://zerobyte.app/docs/guides/provisioning).
See `examples/provisioned-resources/README.md` for a full example.
### Simplified setup (no remote mounts)
If you only need to back up locally-mounted folders and don't require remote share mounting capabilities, you can remove the `SYS_ADMIN` capability and FUSE device from your `docker-compose.yml`:
```yaml
services:
zerobyte:
image: ghcr.io/nicotsx/zerobyte:v0.38
container_name: zerobyte
restart: unless-stopped
ports:
- "4096:4096"
environment:
- TZ=Europe/Zurich # Set your timezone here
- BASE_URL=http://localhost:4096 # Change this to your actual URL (use https:// for secure cookies)
- APP_SECRET=94bad46...c66e25d5c2b # Generate your own secret with `openssl rand -hex 32`
volumes:
- /etc/localtime:/etc/localtime:ro
- /var/lib/zerobyte:/var/lib/zerobyte
- /path/to/your/directory:/mydata
```
**Trade-offs:**
- ✅ Improved security by reducing container capabilities
- ✅ Support for local directories as backup sources
- ✅ Support all repository types, local and remote (S3, GCS, Azure, rclone)
- ❌ Cannot mount NFS, SMB, WebDAV, or SFTP shares directly from Zerobyte
If you need remote mount capabilities, keep the original configuration with `cap_add: SYS_ADMIN` and `devices: /dev/fuse:/dev/fuse`.
## Examples
See [examples/README.md](examples/README.md) for runnable, copy/paste-friendly examples.
## Adding your first volume
Ironmount supports multiple volume backends including NFS, SMB, WebDAV, and local directories. A volume represents the source data you want to back up and monitor.
Zerobyte supports multiple volume backends including NFS, SMB, WebDAV, SFTP, and local directories. A volume represents the source data you want to back up and monitor.
To add your first volume, navigate to the "Volumes" section in the web interface and click on "Create volume". Fill in the required details such as volume name, type, and connection settings.
If you want to track a local directory on the same server where Ironmount is running, you'll first need to mount that directory into the Ironmount container. You can do this by adding a volume mapping in your `docker-compose.yml` file. For example, to mount `/path/to/your/directory` from the host to `/mydata` in the container, you would add the following line under the `volumes` section:
If you want to backup a local directory on the same host where Zerobyte is running, you'll first need to mount that directory into the Zerobyte container. You can do this by adding a volume mapping in your `docker-compose.yml` file. For example, to mount `/path/to/your/directory` from the host to `/mydata` in the container, you would add the following line under the `volumes` section:
```diff
services:
ironmount:
image: ghcr.io/nicotsx/ironmount:v0.10
container_name: ironmount
zerobyte:
image: ghcr.io/nicotsx/zerobyte:v0.38
container_name: zerobyte
restart: unless-stopped
cap_add:
- SYS_ADMIN
@ -81,32 +204,36 @@ services:
- "4096:4096"
devices:
- /dev/fuse:/dev/fuse
environment:
- TZ=Europe/Zurich
- BASE_URL=http://localhost:4096 # URL you will use to access Zerobyte
- APP_SECRET=94bad46...c66e25d5c2b # Generate your own secret with `openssl rand -hex 32`
volumes:
- /etc/localtime:/etc/localtime:ro
- /var/lib/ironmount:/var/lib/ironmount
- /var/lib/zerobyte:/var/lib/zerobyte
+ - /path/to/your/directory:/mydata
```
After updating the `docker-compose.yml` file, restart the Ironmount container to apply the changes:
After updating the `docker-compose.yml` file, restart the Zerobyte container to apply the changes:
```bash
docker compose down
docker compose up -d
```
Now, when adding a new volume in the Ironmount web interface, you can select "Directory" as the volume type and search for your mounted path (e.g., `/mydata`) as the source path.
Now, when adding a new volume in the Zerobyte web interface, you can select "Directory" as the volume type and search for your mounted path (e.g., `/mydata`) as the source path.
![Preview](https://github.com/nicotsx/ironmount/blob/main/screenshots/add-volume.png?raw=true)
![Add a new volume UI](https://github.com/nicotsx/zerobyte/blob/main/screenshots/add-volume.png?raw=true)
## Creating a repository
A repository is where your backups will be securely stored encrypted. Ironmount supports multiple storage backends for your backup repositories:
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/ironmount/repositories/<repository-name>`
- **S3-compatible storage** - Amazon S3, MinIO, Wasabi, DigitalOcean Spaces, etc.
- **Google Cloud Storage** - Google's cloud storage service
- **Azure Blob Storage** - Microsoft Azure storage
- **rclone remotes** - 40+ cloud storage providers via rclone (see below)
- **Local directories**: Store backups on local disk subfolders in `/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
- **rclone remotes**: 40+ cloud storage providers via rclone (see below)
Repositories are optimized for storage efficiency and data integrity, leveraging Restic's deduplication and encryption features.
@ -114,32 +241,37 @@ To create a repository, navigate to the "Repositories" section in the web interf
### Using rclone for cloud storage
Ironmount can use [rclone](https://rclone.org/) to support 40+ cloud storage providers including Google Drive, Dropbox, OneDrive, Box, pCloud, Mega, and many more. This gives you the flexibility to store your backups on virtually any cloud storage service.
Zerobyte can use [rclone](https://rclone.org/) to support 40+ cloud storage providers including Google Drive, Dropbox, OneDrive, Box, pCloud, Mega, and many more. This gives you the flexibility to store your backups on virtually any cloud storage service.
**Setup instructions:**
1. **Install rclone on your host system** (if not already installed):
```bash
curl https://rclone.org/install.sh | sudo bash
```
2. **Configure your cloud storage remote** using rclone's interactive config:
```bash
rclone config
```
Follow the prompts to set up your cloud storage provider. For OAuth providers (Google Drive, Dropbox, etc.), rclone will guide you through the authentication flow.
3. **Verify your remote is configured**:
```bash
rclone listremotes
```
4. **Mount the rclone config into the Ironmount container** by updating your `docker-compose.yml`:
4. **Mount the rclone config into the Zerobyte container** by updating your `docker-compose.yml`:
```diff
services:
ironmount:
image: ghcr.io/nicotsx/ironmount:v0.10
container_name: ironmount
zerobyte:
image: ghcr.io/nicotsx/zerobyte:v0.38
container_name: zerobyte
restart: unless-stopped
cap_add:
- SYS_ADMIN
@ -147,22 +279,35 @@ Ironmount can use [rclone](https://rclone.org/) to support 40+ cloud storage pro
- "4096:4096"
devices:
- /dev/fuse:/dev/fuse
environment:
- TZ=Europe/Zurich
- BASE_URL=http://localhost:4096 # URL you will use to access Zerobyte
volumes:
- /etc/localtime:/etc/localtime:ro
- /var/lib/ironmount:/var/lib/ironmount
+ - ~/.config/rclone:/root/.config/rclone
- /var/lib/zerobyte:/var/lib/zerobyte
+ - ~/.config/rclone:/root/.config/rclone:ro
```
5. **Restart the Ironmount container**:
> **Note for non-root users:** If your container runs as a different user (e.g., TrueNAS apps), mount your config to the appropriate location and set `RCLONE_CONFIG_DIR`:
>
> ```yaml
> environment:
> - RCLONE_CONFIG_DIR=/home/appuser/.config/rclone
> volumes:
> - ~/.config/rclone:/home/appuser/.config/rclone:ro
> ```
5. **Restart the Zerobyte container**:
```bash
docker compose down
docker compose up -d
```
6. **Create a repository** in Ironmount:
6. **Create a repository** in Zerobyte:
- Select "rclone" as the repository type
- Choose your configured remote from the dropdown
- Specify the path within your remote (e.g., `backups/ironmount`)
- Specify the path within your remote (e.g., `backups/zerobyte`)
For a complete list of supported providers, see the [rclone documentation](https://rclone.org/).
@ -171,115 +316,63 @@ For a complete list of supported providers, see the [rclone documentation](https
Once you have added a volume and created a repository, you can create your first backup job. A backup job defines the schedule and parameters for backing up a specific volume to a designated repository.
When creating a backup job, you can specify the following settings:
- **Schedule**: Define how often the backup should run (e.g., daily, weekly)
- **Retention Policy**: Set rules for how long backups should be retained (e.g., keep daily backups for 7 days, weekly backups for 4 weeks)
- **Paths**: Specify which files or directories to include in the backup
After configuring the backup job, save it and Ironmount will automatically execute the backup according to the defined schedule.
After configuring the backup job, save it and Zerobyte will automatically execute the backup according to the defined schedule.
You can monitor the progress and status of your backup jobs in the "Backups" section of the web interface.
![Preview](https://github.com/nicotsx/ironmount/blob/main/screenshots/backups-list.png?raw=true)
![Backups UI](https://github.com/nicotsx/zerobyte/blob/main/screenshots/backups-list.png?raw=true)
## Restoring data
Ironmount allows you to easily restore your data from backups. To restore data, navigate to the "Backups" section and select the backup job from which you want to restore data. You can then choose a specific backup snapshot and select the files or directories you wish to restore. The data you select will be restored to their original location.
Zerobyte allows you to easily restore your data from backups. To restore data, navigate to the "Backups" section and select the backup job from which you want to restore data. You can then choose a specific backup snapshot and select the files or directories you wish to restore. The data you select will be restored to their original location.
![Preview](https://github.com/nicotsx/ironmount/blob/main/screenshots/restoring.png?raw=true)
![Restore UI](https://github.com/nicotsx/zerobyte/blob/main/screenshots/restoring.png?raw=true)
## Propagating mounts to host
## Authentication
Ironmount is capable of propagating mounted volumes from within the container to the host system. This is particularly useful when you want to access the mounted data directly from the host to use it with other applications or services.
Zerobyte uses [better-auth](https://github.com/better-auth/better-auth) for authentication and session management. The authentication system automatically adapts to your deployment scenario:
In order to enable this feature, you need to change your bind mount `/var/lib/ironmount` to use the `:rshared` flag. Here is an example of how to set this up in your `docker-compose.yml` file:
### Users and roles
```diff
services:
ironmount:
image: ghcr.io/nicotsx/ironmount:v0.10
container_name: ironmount
restart: unless-stopped
ports:
- "4096:4096"
devices:
- /dev/fuse:/dev/fuse
volumes:
- /etc/localtime:/etc/localtime:ro
- - /var/lib/ironmount:/var/lib/ironmount
+ - /var/lib/ironmount:/var/lib/ironmount:rshared
```
Zerobyte does not currently provide fine-grained RBAC for backup operations. Authenticated organization members are trusted operators for repositories, volumes, backup schedules, restores, and notification destinations in their organization.
Restart the Ironmount container to apply the changes:
Organization roles mainly restrict organization-management and instance-management actions. For example, normal members cannot manage members, SSO settings, invitations, registration, or global users, but they can still operate backup resources. Only add users to an organization if you are comfortable with them using the storage, volume, and notification capabilities configured for the instance.
```bash
docker compose down
docker compose up -d
```
### Cookie security
## Docker plugin
- **IP Address/HTTP access**: Set `BASE_URL=http://192.168.1.50:4096` (or your IP). Cookies will use `Secure: false`, allowing immediate login without SSL.
- **Domain/HTTPS access**: Set `BASE_URL=https://zerobyte.example.com`. Cookies will automatically use `Secure: true` for protected sessions.
Ironmount can also be used as a Docker volume plugin, allowing you to mount your volumes directly into other Docker containers. This enables seamless integration with your containerized applications.
### Reverse proxy setup
In order to enable this feature, you need to run Ironmount with several items shared from the host. Here is an example of how to set this up in your `docker-compose.yml` file:
If you're running Zerobyte behind a reverse proxy (Nginx, Traefik, Caddy, etc.):
```diff
services:
ironmount:
image: ghcr.io/nicotsx/ironmount:v0.10
container_name: ironmount
restart: unless-stopped
cap_add:
- SYS_ADMIN
ports:
- "4096:4096"
devices:
- /dev/fuse:/dev/fuse
volumes:
- /etc/localtime:/etc/localtime:ro
- - /var/lib/ironmount:/var/lib/ironmount
+ - /var/lib/ironmount:/var/lib/ironmount:rshared
+ - /run/docker/plugins:/run/docker/plugins
+ - /var/run/docker.sock:/var/run/docker.sock
```
1. **Set `BASE_URL`** to your HTTPS domain (e.g., `https://zerobyte.example.com`)
2. The app will automatically enable secure cookies based on the `https://` prefix
3. Ensure your proxy passes the `X-Forwarded-Proto` header
Restart the Ironmount container to apply the changes:
### Important notes
```bash
docker compose down
docker compose up -d
```
- The `BASE_URL` must start with `https://` for secure cookies to be enabled
- Local IP addresses (e.g., `http://192.168.x.x`) are **not** treated as secure contexts by browsers, so secure cookies are disabled automatically
- `TRUSTED_ORIGINS` only allows additional origins for auth-related requests. It does not disable secure cookies or make authenticated HTTP access work when `BASE_URL` is HTTPS.
- If `BASE_URL` is HTTPS, browsers will only send Zerobyte's auth cookies over HTTPS. Plain HTTP access may still show the login page, but authenticated flows will fail because no session cookie is available.
Your Ironmount volumes will now be available as Docker volumes that you can mount into other containers using the `--volume` flag:
## Troubleshooting
```bash
docker run -v im-nfs:/path/in/container nginx:latest
```
For troubleshooting common issues, please refer to the [TROUBLESHOOTING.md](TROUBLESHOOTING.md) file.
Or using Docker Compose:
```yaml
services:
myservice:
image: nginx:latest
volumes:
- im-nfs:/path/in/container
volumes:
im-nfs:
external: true
```
The volume name format is `im-<volume-name>` where `<volume-name>` is the name you assigned to the volume in Ironmount. You can verify that the volume is available by running:
```bash
docker volume ls
```
## Third-Party Software
## Third-party software
This project includes the following third-party software components:
### Restic
Ironmount includes [Restic](https://github.com/restic/restic) for backup functionality.
Zerobyte includes [Restic](https://github.com/restic/restic) for backup functionality.
- **License**: BSD 2-Clause License
- **Copyright**: Copyright (c) 2014, Alexander Neumann <alexander@bumpern.de>
@ -287,3 +380,34 @@ Ironmount includes [Restic](https://github.com/restic/restic) for backup functio
- **License Text**: See [LICENSES/BSD-2-Clause-Restic.txt](LICENSES/BSD-2-Clause-Restic.txt)
For a complete list of third-party software licenses and attributions, please refer to the [NOTICES.md](NOTICES.md) file.
## Contributing
Contributions by anyone are welcome! If you find a bug or have a feature request, please open an issue on GitHub. If you want to contribute code, feel free to fork the repository and submit a pull request. We require that all contributors sign a Contributor License Agreement (CLA) before we can accept your contributions. This is to protect both you and the project. Please see the [CONTRIBUTING.md](CONTRIBUTING.md) file for more details.
## Development (no Docker)
After installing Vite+ globally, you can run Zerobyte locally during development without Docker:
```bash
vp install
bun run dev
```
For local development, create a `.env.local` file at the repo root and override the Docker paths:
```bash
# Example
ZEROBYTE_DATABASE_URL=./data/zerobyte.db
APP_SECRET=your_app_secret_here
RESTIC_PASS_FILE=./data/restic.pass
RESTIC_CACHE_DIR=./data/restic/cache
ZEROBYTE_REPOSITORIES_DIR=./data/repositories
ZEROBYTE_VOLUMES_DIR=./data/volumes
BASE_URL=https://*.localhost
```
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.

75
SECURITY.md Normal file
View file

@ -0,0 +1,75 @@
# Security Policy
Thank you for helping keep Zerobyte and its users secure.
Zerobyte is currently in `0.x`, and releases may include breaking changes between versions. For that reason, security fixes are only guaranteed for the most recent stable release line.
## Supported Versions
| Version | Supported |
| --------------------------------------------- | ------------------ |
| Latest stable `0.x` release | :white_check_mark: |
| Older stable `0.x` releases | :x: |
| Pre-release builds (`-alpha`, `-beta`, `-rc`) | :x: |
| Development snapshots from `main` | :x: |
Security fixes are generally released in the latest stable version only. If you are running an older release, you may be asked to upgrade before a fix is provided.
## Reporting a Vulnerability
Please do **not** report security vulnerabilities through public GitHub issues, discussions, or pull requests.
Use one of these private channels instead:
1. Preferred: GitHub private vulnerability reporting
https://github.com/nicotsx/zerobyte/security/advisories/new
2. Alternative: contact the maintainer directly by email if an address is listed in the repository or GitHub profile
When reporting, include as much of the following as you can:
- affected Zerobyte version
- deployment details, including whether you are using Docker, reverse proxies, or exposed ports
- clear reproduction steps or a proof of concept
- impact assessment and what an attacker could do
- any relevant logs, screenshots, or configuration excerpts with secrets removed
### What to expect
- We aim to acknowledge new reports within 7 days.
- We aim to provide status updates at least every 7 days while the report is being investigated.
- If the report is accepted, we will work on a fix, coordinate disclosure, and publish a security advisory when appropriate.
- If the report is declined, out of scope, or cannot be reproduced, we will explain why when possible.
Please avoid public disclosure until a fix has been released and maintainers have had reasonable time to notify users.
## Trust model baseline
Zerobyte is a self-hosted operator tool. Treat any authenticated user as a trusted machine/operator user with intentional access to:
- Browse/select host directories for volumes
- Configure local, network, and cloud storage backends
- Trigger mounts/unmounts, backups, restores, and Restic maintenance
- Read/write files through intended backup/restore workflows
- Access repository/volume metadata needed to operate backups
Do **not** report these as vulnerabilities by themselves:
- Authenticated host filesystem browsing
- Local directory volume pointing to broad host paths
- Backing up arbitrary readable host paths
- Restoring snapshots to arbitrary writable host paths
- Authenticated Restic/mount/rclone execution through intended UI flows
- Information disclosure to authenticated operators about filesystem paths or backend errors
Only report issues when they violate this trust model, for example:
- Unauthenticated access to operator features
- CSRF/cross-origin abuse causing a trusted operators browser to perform actions
- Shell/command injection beyond intended argument-based execution
- Path traversal that escapes a deliberately configured root/volume/repository boundary
- Secret leakage to logs, unauthenticated users, or non-operator contexts
- Cross-organization data access despite authenticated trust
- Privilege bypass between global admin/org admin/member where the product explicitly distinguishes roles
- Unsafe dev-only features enabled without the documented gate
- Vulnerabilities in parsing untrusted external data from repositories/backends/notifications
- Persistence corruption, data loss, or workflow bypass not intended by operator actions

474
TROUBLESHOOTING.md Normal file
View file

@ -0,0 +1,474 @@
# Troubleshooting
If you encounter any issues while using Zerobyte, you can check the application logs for more information.
These logs can help you identify and resolve common problems; you should also check existing and closed issues on GitHub.
In case you need further assistance, feel free to open a new issue with detailed information about the problem you are facing and any relevant log entries.
> [!WARNING]
> Make sure to never share sensitive information such as passwords, access keys, or personal data in public issues so remove them from logs before posting.
Before troubleshooting, enable debug logging so the logs contain enough detail to diagnose issues.
```yaml
services:
zerobyte:
environment:
- LOG_LEVEL=debug
```
After adding `LOG_LEVEL=debug`, restart Zerobyte and then collect logs.
To view the logs, run the command below:
```bash
# replace 'zerobyte' with your container name if different
docker logs -f zerobyte
```
---
## Table of Contents
- [Common Issues](#common-issues)
- [Permission Denied Errors When Mounting Remote Shares](#permission-denied-errors-when-mounting-remote-shares)
- [Security Levels for Mounting Remote Shares](#security-levels-for-mounting-remote-shares)
- [Permission Errors with Remote Shares](#permission-errors-with-remote-shares)
- [Container Cannot Perform Mounts](#container-cannot-perform-mounts)
- [AppArmor-Enabled Systems (Ubuntu/Debian)](#apparmor-enabled-systems-ubuntudebian)
- [Seccomp-Restricted Environments](#seccomp-restricted-environments)
- [SELinux-Enabled Systems (CentOS/Fedora)](#selinux-enabled-systems-centosfedora)
- [Still Getting Permission Errors After SYS_ADMIN](#still-getting-permission-errors-after-sys_admin)
- [FUSE Mount Failures](#fuse-mount-failures)
- [Rclone Issues](#rclone-issues)
- [Test on Host First](#critical-test-on-host-first)
- [Pre-flight Checklist](#pre-flight-checklist)
- [Common Rclone Errors](#common-rclone-errors)
- ["No Remotes Available" in Dropdown](#no-remotes-available-in-dropdown)
- ["Failed to Create File System" Error](#failed-to-create-file-system-error)
- [EACCES Errors](#eacces-errors)
- [Rclone SFTP Repository Authentication Failures](#rclone-sftp-repository-authentication-failures)
- [Rclone Volume Mount Issues](#rclone-volume-mount-issues)
- [Prerequisites Check](#prerequisites-check)
- [Common Mount Errors](#common-mount-errors)
- [Still Having Issues?](#still-having-issues)
---
## Common Issues
### Permission Denied Errors When Mounting Remote Shares
Mounting remote filesystems (such as SMB/CIFS) requires kernel-level privileges. When Zerobyte attempts to perform mounts from inside a container, additional permissions may be required.
Ensure that:
- Remote share credentials are correct
- The host kernel supports the target filesystem (e.g. CIFS module is available)
- Docker is running in **rootful mode** (rootless Docker cannot perform kernel mounts)
In some environments, Linux security mechanisms such as AppArmor or seccomp may block mount-related operations even when the required capabilities are present.
---
### Security Levels for Mounting Remote Shares
Zerobyte supports multiple deployment models depending on your security requirements and environment.
---
#### Permission Errors with Remote Shares
**Problem:** You're getting permission errors when Zerobyte tries to mount remote shares (SMB/CIFS, NFS, etc.).
**Solution:** Mount remote shares **outside of Zerobyte** (on the host) and point Zerobyte to an already-mounted local path. This avoids container permission issues entirely.
**How to fix:**
1. Mount your remote share on the host first using `systemd`, `autofs`, or manual mount:
```bash
sudo mount -t cifs //server/share /mnt/your-remote-share -o credentials=/path/to/creds
```
2. Then mount that local path into Zerobyte:
```yaml
services:
zerobyte:
volumes:
- /mnt/your-remote-share:/data
```
3. Restart the container:
```bash
docker compose down && docker compose up -d
```
---
#### Container Cannot Perform Mounts
**Problem:** Zerobyte shows "Operation not permitted" errors when trying to mount remote shares directly.
**Solution:** Grant the `SYS_ADMIN` capability to allow the container to perform mount operations.
**Warning:** This grants significant privileges to the container and should only be used when necessary.
```yaml
services:
zerobyte:
cap_add:
- SYS_ADMIN
```
> ⚠️ Granting `SYS_ADMIN` allows the container to perform mount operations and should be used only when strictly necessary.
---
#### AppArmor-Enabled Systems (Ubuntu/Debian)
On hosts using AppArmor, the default Docker profile (`docker-default`) may block mount operations even when `SYS_ADMIN` is present.
If mount operations fail with permission errors, you may need to disable AppArmor confinement for the container. Check first if AppArmor is enabled on your system and the profile of the container:
```bash
# check if AppArmor is enabled
sudo aa-status
# if next command returns 'docker-default', AppArmor is enabled on the container
docker inspect --format='{{.AppArmorProfile}}' zerobyte
```
If AppArmor is enabled, you can disable it for the Zerobyte container by adding the following to your `docker-compose.yml`:
```yaml
services:
zerobyte:
cap_add:
- SYS_ADMIN
security_opt:
- apparmor:unconfined
```
---
#### Seccomp-Restricted Environments
Docker's default seccomp profile may block mount-related syscalls required by filesystem operations.
If mount operations continue to fail, you may need to disable seccomp filtering for the container:
```yaml
services:
zerobyte:
cap_add:
- SYS_ADMIN
security_opt:
- seccomp:unconfined
```
---
#### SELinux-Enabled Systems (CentOS/Fedora)
On hosts using SELinux, you may need to adjust the security context to allow mount operations.
If mount operations fail with permission errors, you can try adding the following label:
```yaml
services:
zerobyte:
cap_add:
- SYS_ADMIN
security_opt:
- label:type:container_runtime_t
```
or disable SELinux enforcement for the container:
```yaml
services:
zerobyte:
cap_add:
- SYS_ADMIN
security_opt:
- label:disable
```
---
#### Still Getting Permission Errors After SYS_ADMIN
**Problem:** Mount operations still fail even with `SYS_ADMIN` capability.
**Solution (Last Resort):** Run the container in privileged mode. This disables most container isolation mechanisms and significantly increases the attack surface.
**Warning:** Only use this as a last resort for troubleshooting. Remove and switch to a more secure solution once you identify the actual issue.
```yaml
services:
zerobyte:
privileged: true
```
---
### FUSE Mount Failures
**Problem:** FUSE-based mounts (sshfs, rclone mount) are failing with device access errors.
**Cause:** Access to `/dev/fuse` is required for FUSE-based filesystems.
**Solution:** Ensure `/dev/fuse` is mounted into the container. This is **not required** for SMB/CIFS mounts.
````yaml
services:
zerobyte:
devices:
- /dev/fuse:/dev/fuse
---
## Rclone Issues
### ⚠️ Critical: Test on Host First
**Before reporting any rclone-related issue, you MUST verify that rclone works correctly on your Docker host.**
Most rclone issues are due to misconfigured remotes, not Zerobyte bugs. Follow this checklist on your host machine:
```bash
# 1. List all configured remotes
rclone listremotes
# 2. Test listing a remote (replace 'myremote' with your remote name)
rclone lsd myremote:
# 3. Test reading from a remote
rclone ls myremote:path/to/test
# 4. For volume mounts: Test mounting on host first
mkdir -p /tmp/rclone-test
rclone mount myremote:path /tmp/rclone-test --daemon --vfs-cache-mode writes
ls /tmp/rclone-test
fusermount -u /tmp/rclone-test # Unmount when done
````
**If these commands fail on your host, fix your rclone configuration before using Zerobyte.**
Common issues include:
- Expired OAuth tokens (run `rclone config` to re-authenticate)
- Incorrect credentials
- Missing permissions on cloud provider side
- Network/firewall issues
---
### Pre-flight Checklist
If you're experiencing rclone issues, verify all of the following:
- [ ] Rclone is installed and configured on the Docker **host**
- [ ] `rclone listremotes` shows your remote
- [ ] `rclone lsd remote:` successfully lists directories
- [ ] The rclone config directory is mounted into the container
---
### Common Rclone Errors
#### "No Remotes Available" in Dropdown
**Cause:** Zerobyte cannot find your rclone configuration file.
**Diagnosis:**
```bash
# Check which config file rclone will use inside the container
docker exec zerobyte sh -lc 'echo HOME=$HOME; rclone config file'
```
**Solutions:**
1. Ensure you've mounted your host's rclone config:
```yaml
volumes:
- ~/.config/rclone:/root/.config/rclone:ro
```
2. For non-root containers (e.g., TrueNAS), set the correct path:
```yaml
environment:
- RCLONE_CONFIG_DIR=/home/appuser/.config/rclone
volumes:
- ~/.config/rclone:/home/appuser/.config/rclone:ro
```
3. **Restart the container** after mounting the config:
```bash
docker compose down
docker compose up -d
```
#### "Failed to Create File System" Error
**Cause:** Authentication failure with the cloud provider.
**Solution:**
1. On your host, run: `rclone config`
2. Re-authenticate the remote (especially for OAuth providers like Google Drive, Dropbox, OneDrive)
3. Verify with: `rclone lsd remote:`
4. Restart the Zerobyte container
#### EACCES Errors
**Cause:** AppArmor or seccomp is blocking rclone execution.
**Solution:** Disable security profiles as described in:
- [AppArmor-enabled systems](#apparmor-enabled-systems-ubuntudebian)
- [Seccomp-restricted environments](#seccomp-restricted-environments)
#### Rclone SFTP Repository Authentication Failures
When creating an **rclone repository** that uses an **SFTP remote**, you may encounter authentication errors even though:
- The rclone config is mounted correctly
- The SFTP remote appears in the dropdown
- The same config works on the host
**The issue:** If your rclone SFTP remote uses `key_file` for SSH key authentication, the key file path in your rclone config points to a location on the **host** (e.g., `~/.ssh/id_rsa`). When rclone runs inside the container, it cannot access that path.
**Solutions:**
**Option 1: Mount your SSH keys (Recommended)**
Add your SSH directory to the container volumes:
```yaml
services:
zerobyte:
volumes:
- ~/.config/rclone:/root/.config/rclone:ro
- ~/.ssh:/root/.ssh:ro # Required for SFTP remotes using key_file
```
**Option 2: Embed the SSH key in rclone config**
Use `key_pem` instead of `key_file` to embed the private key directly in your rclone.conf:
```bash
# Convert your key to single-line format
awk '{printf "%s\\n", $0}' < ~/.ssh/id_rsa
```
Then update your rclone config:
```ini
[sftp-remote]
type = sftp
host = example.com
user = backup
key_pem = -----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAABG5v...\n-----END OPENSSH PRIVATE KEY-----
```
**Option 3: Use ssh-agent**
Configure your rclone remote to use ssh-agent instead of a key file:
```ini
[sftp-remote]
type = sftp
host = example.com
user = backup
key_use_agent = true
```
Then mount the SSH agent socket and set the environment variable:
```yaml
services:
zerobyte:
environment:
- SSH_AUTH_SOCK=/ssh-agent
volumes:
- ~/.config/rclone:/root/.config/rclone:ro
- ${SSH_AUTH_SOCK}:/ssh-agent
```
---
### Rclone Volume Mount Issues
When using rclone as a **volume backend** (mounting cloud storage to back up from), additional requirements apply:
#### Prerequisites Check
If rclone volume mounting isn't working, verify these prerequisites on your system:
- **Linux host required:** Windows/macOS hosts cannot use rclone volumes. If you're on these platforms, you'll need to use a different volume backend.
- **`/dev/fuse` device:** Check if FUSE is available and properly passed to the container
- **`SYS_ADMIN` capability:** Required for mount operations inside containers
- **FUSE support on host:** Verify FUSE is installed and working on your Docker host
#### Common Mount Errors
**"mount helper error: fusermount3: failed to open /dev/fuse: Permission denied"**
Solutions in order of preference:
1. **Verify FUSE works on host first:**
```bash
# On host
rclone mount remote:path /tmp/test --daemon
ls /tmp/test
fusermount -u /tmp/test
```
2. **Ensure /dev/fuse is mounted in container:**
```yaml
devices:
- /dev/fuse:/dev/fuse
```
3. **Check user permissions inside container:**
```bash
docker exec zerobyte ls -la /dev/fuse
```
**"mount helper error: fusermount3: mount failed: Operation not permitted"**
This indicates the container lacks `SYS_ADMIN` capability:
```yaml
cap_add:
- SYS_ADMIN
```
---
### Still having issues?
If you've verified rclone works on the host and followed all troubleshooting steps above, gather this information for your issue report:
```bash
# Host verification
rclone version
rclone listremotes
rclone lsd remote: 2>&1
# Container verification
docker exec zerobyte ls -la /root/.config/rclone/
docker exec zerobyte env | grep RCLONE
docker logs zerobyte 2>&1 | tail -50
# Check container capabilities
docker inspect zerobyte --format='{{.HostConfig.CapAdd}}'
docker inspect zerobyte --format='{{.HostConfig.Devices}}'
```
Include the output (with sensitive data redacted) in your GitHub issue.

View file

@ -1,10 +1,20 @@
@import "tailwindcss";
@plugin "@tailwindcss/typography";
@import "tw-animate-css";
@import "dither-plugin";
@custom-variant dark (&:is(.dark *));
@custom-variant @narrow (@container (min-width: 28rem));
@custom-variant @medium (@container (min-width: 40rem));
@custom-variant @wide (@container (min-width: 60rem));
@custom-variant @xwide (@container (min-width: 80rem));
@theme {
--container-narrow: 28rem;
--container-medium: 40rem;
--container-wide: 60rem;
--container-xwide: 80rem;
--breakpoint-xs: 32rem;
--font-sans:
"Google Sans Code", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol",
@ -16,12 +26,17 @@ body {
overflow-x: hidden;
width: 100%;
position: relative;
overscroll-behavior: none;
scrollbar-width: thin;
}
@media (min-width: 48rem) {
html,
body {
overscroll-behavior: none;
}
}
body {
@apply bg-[#131313];
min-height: 100dvh;
}
@ -51,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);
@ -72,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.98 0.002 250);
--foreground: oklch(0.20 0.01 260);
--card: #ffffff;
--card-foreground: oklch(0.20 0.01 260);
--card-header: oklch(0.99 0.001 260);
--popover: #ffffff;
--popover-foreground: oklch(0.20 0.01 260);
--primary: oklch(0.20 0.01 260);
--primary-foreground: #ffffff;
--secondary: oklch(0.96 0.005 260);
--secondary-foreground: oklch(0.20 0.01 260);
--muted: oklch(0.95 0.005 260);
--muted-foreground: oklch(0.45 0.01 260);
--accent: oklch(0.96 0.005 260);
--accent-foreground: oklch(0.20 0.01 260);
--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);
--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);
--destructive-foreground: #fff;
--success: oklch(0.57 0.16 148);
--border: oklch(0.90 0.01 260);
--input: oklch(0.90 0.01 260);
--ring: oklch(0.7 0.01 260);
--chart-1: oklch(0.55 0.22 260);
--chart-2: oklch(0.6 0.15 200);
--chart-3: oklch(0.45 0.12 280);
--chart-4: oklch(0.7 0.15 190);
--chart-5: oklch(0.55 0.18 300);
--sidebar: #ffffff;
--sidebar-foreground: oklch(0.35 0.01 260);
--sidebar-primary: oklch(0.20 0.01 260);
--sidebar-primary-foreground: #ffffff;
--sidebar-accent: oklch(0.97 0.005 260);
--sidebar-accent-foreground: oklch(0.20 0.01 260);
--sidebar-border: oklch(0.90 0.01 260);
--sidebar-ring: oklch(0.7 0.01 260);
--strong-accent: #ff543a;
}
@ -128,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);
@ -158,11 +179,11 @@ body {
@layer base {
:root {
--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);
--chart-1: oklch(0.55 0.22 260);
--chart-2: oklch(0.6 0.15 200);
--chart-3: oklch(0.45 0.12 280);
--chart-4: oklch(0.7 0.15 190);
--chart-5: oklch(0.55 0.18 300);
}
.dark {
@ -173,3 +194,20 @@ body {
--chart-5: oklch(0.645 0.246 16.439);
}
}
.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 {
display: none;
}

20
app/client.tsx Normal file
View file

@ -0,0 +1,20 @@
import { StrictMode, startTransition } from "react";
import { hydrateRoot } from "react-dom/client";
import { StartClient } from "@tanstack/react-start/client";
import { parseError } from "./client/lib/errors";
startTransition(() => {
hydrateRoot(
document,
<StrictMode>
<StartClient />
</StrictMode>,
{
onRecoverableError: (error, errorInfo) => {
console.error(
`[react-recoverable-error] ${parseError(error)?.message}${errorInfo.componentStack ? `\n${errorInfo.componentStack.trim()}` : ""}`,
);
},
},
);
});

File diff suppressed because it is too large Load diff

View file

@ -1,18 +1,27 @@
// @ts-nocheck
// This file is auto-generated by @hey-api/openapi-ts
import { type ClientOptions, type Config, createClient, createConfig } from "./client";
import type { ClientOptions as ClientOptions2 } from "./types.gen";
import { getRequestClient } from "../../lib/request-client";
import { type ClientOptions, type Config, createClient, createConfig } from './client';
import type { ClientOptions as ClientOptions2 } from './types.gen';
export type CreateClientConfig<T extends ClientOptions = ClientOptions2> = (
override?: Config<ClientOptions & T>,
) => Config<Required<ClientOptions> & T>;
const fallbackClient = createClient(createConfig<ClientOptions2>({ baseUrl: "http://localhost:4096" }));
/**
* The `createClientConfig()` function will be called on client initialization
* and the returned object will become the client's initial configuration.
*
* You may want to initialize your client this way instead of calling
* `setConfig()`. This is useful for example if you're using Next.js
* to ensure your client always has the correct values.
* Proxy client that automatically uses the per-request client from request-scoped server storage.
* Falls back to a default client if no request context is available.
*/
export type CreateClientConfig<T extends ClientOptions = ClientOptions2> = (override?: Config<ClientOptions & T>) => Config<Required<ClientOptions> & T>;
export const client = createClient(createConfig<ClientOptions2>({
baseUrl: 'http://192.168.2.42:4096'
}));
export const client = new Proxy(fallbackClient, {
get(target, prop, receiver) {
try {
const requestClient = getRequestClient();
return Reflect.get(requestClient, prop, receiver);
} catch {
// No request context available, use fallback
return Reflect.get(target, prop, receiver);
}
},
});

View file

@ -1,14 +1,10 @@
// @ts-nocheck
// This file is auto-generated by @hey-api/openapi-ts
import { createSseClient } from '../core/serverSentEvents.gen';
import type { HttpMethod } from '../core/types.gen';
import { getValidRequestBody } from '../core/utils.gen';
import type {
Client,
Config,
RequestOptions,
ResolvedRequestOptions,
} from './types.gen';
import type { Client, Config, RequestOptions, ResolvedRequestOptions } from './types.gen';
import {
buildUrl,
createConfig,
@ -34,27 +30,26 @@ export const createClient = (config: Config = {}): Client => {
return getConfig();
};
const interceptors = createInterceptors<
Request,
Response,
unknown,
ResolvedRequestOptions
>();
const interceptors = createInterceptors<Request, Response, unknown, ResolvedRequestOptions>();
const beforeRequest = async (options: RequestOptions) => {
const beforeRequest = async <
TData = unknown,
TResponseStyle extends 'data' | 'fields' = 'fields',
ThrowOnError extends boolean = boolean,
Url extends string = string,
>(
options: RequestOptions<TData, TResponseStyle, ThrowOnError, Url>,
) => {
const opts = {
..._config,
...options,
fetch: options.fetch ?? _config.fetch ?? globalThis.fetch,
headers: mergeHeaders(_config.headers, options.headers),
serializedBody: undefined,
serializedBody: undefined as string | undefined,
};
if (opts.security) {
await setAuthParams({
...opts,
security: opts.security,
});
await setAuthParams(opts);
}
if (opts.requestValidator) {
@ -62,7 +57,7 @@ export const createClient = (config: Config = {}): Client => {
}
if (opts.body !== undefined && opts.bodySerializer) {
opts.serializedBody = opts.bodySerializer(opts.body);
opts.serializedBody = opts.bodySerializer(opts.body) as string | undefined;
}
// remove Content-Type header if body is empty to avoid sending invalid requests
@ -70,209 +65,191 @@ export const createClient = (config: Config = {}): Client => {
opts.headers.delete('Content-Type');
}
const url = buildUrl(opts);
const resolvedOpts = opts as typeof opts &
ResolvedRequestOptions<TResponseStyle, ThrowOnError, Url>;
const url = buildUrl(resolvedOpts);
return { opts, url };
return { opts: resolvedOpts, url };
};
const request: Client['request'] = async (options) => {
// @ts-expect-error
const { opts, url } = await beforeRequest(options);
const requestInit: ReqInit = {
redirect: 'follow',
...opts,
body: getValidRequestBody(opts),
};
const throwOnError = options.throwOnError ?? _config.throwOnError;
const responseStyle = options.responseStyle ?? _config.responseStyle;
let request = new Request(url, requestInit);
for (const fn of interceptors.request.fns) {
if (fn) {
request = await fn(request, opts);
}
}
// fetch must be assigned here, otherwise it would throw the error:
// TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation
const _fetch = opts.fetch!;
let response: Response;
let request: Request | undefined;
let response: Response | undefined;
try {
response = await _fetch(request);
} catch (error) {
// Handle fetch exceptions (AbortError, network errors, etc.)
let finalError = error;
const { opts, url } = await beforeRequest(options);
const requestInit: ReqInit = {
redirect: 'follow',
...opts,
body: getValidRequestBody(opts),
};
for (const fn of interceptors.error.fns) {
request = new Request(url, requestInit);
for (const fn of interceptors.request.fns) {
if (fn) {
finalError = (await fn(
error,
undefined as any,
request,
opts,
)) as unknown;
request = await fn(request, opts);
}
}
finalError = finalError || ({} as unknown);
// fetch must be assigned here, otherwise it would throw the error:
// TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation
const _fetch = opts.fetch!;
if (opts.throwOnError) {
throw finalError;
response = await _fetch(request);
for (const fn of interceptors.response.fns) {
if (fn) {
response = await fn(response, request, opts);
}
}
// Return error response
return opts.responseStyle === 'data'
? undefined
: {
error: finalError,
request,
response: undefined as any,
};
}
const result = {
request,
response,
};
for (const fn of interceptors.response.fns) {
if (fn) {
response = await fn(response, request, opts);
}
}
if (response.ok) {
const parseAs =
(opts.parseAs === 'auto'
? getParseAs(response.headers.get('Content-Type'))
: opts.parseAs) ?? 'json';
const result = {
request,
response,
};
if (response.status === 204 || response.headers.get('Content-Length') === '0') {
let emptyData: any;
switch (parseAs) {
case 'arrayBuffer':
case 'blob':
case 'text':
emptyData = await response[parseAs]();
break;
case 'formData':
emptyData = new FormData();
break;
case 'stream':
emptyData = response.body;
break;
case 'json':
default:
emptyData = {};
break;
}
return opts.responseStyle === 'data'
? emptyData
: {
data: emptyData,
...result,
};
}
if (response.ok) {
const parseAs =
(opts.parseAs === 'auto'
? getParseAs(response.headers.get('Content-Type'))
: opts.parseAs) ?? 'json';
if (
response.status === 204 ||
response.headers.get('Content-Length') === '0'
) {
let emptyData: any;
let data: any;
switch (parseAs) {
case 'arrayBuffer':
case 'blob':
case 'text':
emptyData = await response[parseAs]();
break;
case 'formData':
emptyData = new FormData();
case 'text':
data = await response[parseAs]();
break;
case 'json': {
// Some servers return 200 with no Content-Length and empty body.
// response.json() would throw; read as text and parse if non-empty.
const text = await response.text();
data = text ? JSON.parse(text) : {};
break;
}
case 'stream':
emptyData = response.body;
break;
case 'json':
default:
emptyData = {};
break;
return opts.responseStyle === 'data'
? response.body
: {
data: response.body,
...result,
};
}
if (parseAs === 'json') {
if (opts.responseValidator) {
await opts.responseValidator(data);
}
if (opts.responseTransformer) {
data = await opts.responseTransformer(data);
}
}
return opts.responseStyle === 'data'
? emptyData
? data
: {
data: emptyData,
data,
...result,
};
}
let data: any;
switch (parseAs) {
case 'arrayBuffer':
case 'blob':
case 'formData':
case 'json':
case 'text':
data = await response[parseAs]();
break;
case 'stream':
return opts.responseStyle === 'data'
? response.body
: {
data: response.body,
...result,
};
const textError = await response.text();
let jsonError: unknown;
try {
jsonError = JSON.parse(textError);
} catch {
// noop
}
if (parseAs === 'json') {
if (opts.responseValidator) {
await opts.responseValidator(data);
}
throw jsonError ?? textError;
} catch (error) {
let finalError = error;
if (opts.responseTransformer) {
data = await opts.responseTransformer(data);
for (const fn of interceptors.error.fns) {
if (fn) {
finalError = await fn(finalError, response, request, options as ResolvedRequestOptions);
}
}
return opts.responseStyle === 'data'
? data
finalError = finalError || {};
if (throwOnError) {
throw finalError;
}
// TODO: we probably want to return error and improve types
return responseStyle === 'data'
? undefined
: {
data,
...result,
error: finalError,
request,
response,
};
}
const textError = await response.text();
let jsonError: unknown;
try {
jsonError = JSON.parse(textError);
} catch {
// noop
}
const error = jsonError ?? textError;
let finalError = error;
for (const fn of interceptors.error.fns) {
if (fn) {
finalError = (await fn(error, response, request, opts)) as string;
}
}
finalError = finalError || ({} as string);
if (opts.throwOnError) {
throw finalError;
}
// TODO: we probably want to return error and improve types
return opts.responseStyle === 'data'
? undefined
: {
error: finalError,
...result,
};
};
const makeMethodFn =
(method: Uppercase<HttpMethod>) => (options: RequestOptions) =>
request({ ...options, method });
const makeMethodFn = (method: Uppercase<HttpMethod>) => (options: RequestOptions) =>
request({ ...options, method });
const makeSseFn =
(method: Uppercase<HttpMethod>) => async (options: RequestOptions) => {
const { opts, url } = await beforeRequest(options);
return createSseClient({
...opts,
body: opts.body as BodyInit | null | undefined,
headers: opts.headers as unknown as Record<string, string>,
method,
onRequest: async (url, init) => {
let request = new Request(url, init);
for (const fn of interceptors.request.fns) {
if (fn) {
request = await fn(request, opts);
}
const makeSseFn = (method: Uppercase<HttpMethod>) => async (options: RequestOptions) => {
const { opts, url } = await beforeRequest(options);
return createSseClient({
...opts,
body: opts.body as BodyInit | null | undefined,
method,
onRequest: async (url, init) => {
let request = new Request(url, init);
for (const fn of interceptors.request.fns) {
if (fn) {
request = await fn(request, opts);
}
return request;
},
url,
});
};
}
return request;
},
serializedBody: getValidRequestBody(opts) as BodyInit | null | undefined,
url,
});
};
const _buildUrl: Client['buildUrl'] = (options) => buildUrl({ ..._config, ...options });
return {
buildUrl,
buildUrl: _buildUrl,
connect: makeMethodFn('CONNECT'),
delete: makeMethodFn('DELETE'),
get: makeMethodFn('GET'),

View file

@ -1,25 +1,22 @@
// @ts-nocheck
// This file is auto-generated by @hey-api/openapi-ts
export type { Auth } from '../core/auth.gen';
export type { QuerySerializerOptions } from '../core/bodySerializer.gen';
export {
formDataBodySerializer,
jsonBodySerializer,
urlSearchParamsBodySerializer,
} from '../core/bodySerializer.gen';
export { buildClientParams } from '../core/params.gen';
export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen';
export { createClient } from './client.gen';
export type { Auth } from "../core/auth.gen";
export type { QuerySerializerOptions } from "../core/bodySerializer.gen";
export { formDataBodySerializer, jsonBodySerializer, urlSearchParamsBodySerializer } from "../core/bodySerializer.gen";
export { buildClientParams } from "../core/params.gen";
export { serializeQueryKeyValue } from "../core/queryKeySerializer.gen";
export { createClient } from "./client.gen";
export type {
Client,
ClientOptions,
Config,
CreateClientConfig,
Options,
RequestOptions,
RequestResult,
ResolvedRequestOptions,
ResponseStyle,
TDataShape,
} from './types.gen';
export { createConfig, mergeHeaders } from './utils.gen';
Client,
ClientOptions,
Config,
CreateClientConfig,
Options,
RequestOptions,
RequestResult,
ResolvedRequestOptions,
ResponseStyle,
TDataShape,
} from "./types.gen";
export { createConfig, mergeHeaders } from "./utils.gen";

View file

@ -1,3 +1,4 @@
// @ts-nocheck
// This file is auto-generated by @hey-api/openapi-ts
import type { Auth } from '../core/auth.gen';
@ -5,17 +6,13 @@ import type {
ServerSentEventsOptions,
ServerSentEventsResult,
} from '../core/serverSentEvents.gen';
import type {
Client as CoreClient,
Config as CoreConfig,
} from '../core/types.gen';
import type { Client as CoreClient, Config as CoreConfig } from '../core/types.gen';
import type { Middleware } from './utils.gen';
export type ResponseStyle = 'data' | 'fields';
export interface Config<T extends ClientOptions = ClientOptions>
extends Omit<RequestInit, 'body' | 'headers' | 'method'>,
CoreConfig {
extends Omit<RequestInit, 'body' | 'headers' | 'method'>, CoreConfig {
/**
* Base URL for all requests made by this client.
*/
@ -42,14 +39,7 @@ export interface Config<T extends ClientOptions = ClientOptions>
*
* @default 'auto'
*/
parseAs?:
| 'arrayBuffer'
| 'auto'
| 'blob'
| 'formData'
| 'json'
| 'stream'
| 'text';
parseAs?: 'arrayBuffer' | 'auto' | 'blob' | 'formData' | 'json' | 'stream' | 'text';
/**
* Should we return only data or multiple fields (data, error, response, etc.)?
*
@ -69,12 +59,15 @@ export interface RequestOptions<
TResponseStyle extends ResponseStyle = 'fields',
ThrowOnError extends boolean = boolean,
Url extends string = string,
> extends Config<{
>
extends
Config<{
responseStyle: TResponseStyle;
throwOnError: ThrowOnError;
}>,
Pick<
ServerSentEventsOptions<TData>,
| 'onRequest'
| 'onSseError'
| 'onSseEvent'
| 'sseDefaultRetryDelay'
@ -101,6 +94,7 @@ export interface ResolvedRequestOptions<
ThrowOnError extends boolean = boolean,
Url extends string = string,
> extends RequestOptions<unknown, TResponseStyle, ThrowOnError, Url> {
headers: Headers;
serializedBody?: string;
}
@ -116,36 +110,28 @@ export type RequestResult<
? TData[keyof TData]
: TData
: {
data: TData extends Record<string, unknown>
? TData[keyof TData]
: TData;
data: TData extends Record<string, unknown> ? TData[keyof TData] : TData;
request: Request;
response: Response;
}
>
: Promise<
TResponseStyle extends 'data'
?
| (TData extends Record<string, unknown>
? TData[keyof TData]
: TData)
| undefined
? (TData extends Record<string, unknown> ? TData[keyof TData] : TData) | undefined
: (
| {
data: TData extends Record<string, unknown>
? TData[keyof TData]
: TData;
data: TData extends Record<string, unknown> ? TData[keyof TData] : TData;
error: undefined;
}
| {
data: undefined;
error: TError extends Record<string, unknown>
? TError[keyof TError]
: TError;
error: TError extends Record<string, unknown> ? TError[keyof TError] : TError;
}
) & {
request: Request;
response: Response;
/** request may be undefined, because error may be from building the request object itself */
request?: Request;
/** response may be undefined, because error may be from building the request object itself or from a network error */
response?: Response;
}
>;
@ -170,7 +156,7 @@ type SseFn = <
ThrowOnError extends boolean = false,
TResponseStyle extends ResponseStyle = 'fields',
>(
options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, 'method'>,
options: Omit<RequestOptions<never, TResponseStyle, ThrowOnError>, 'method'>,
) => Promise<ServerSentEventsResult<TData, TError>>;
type RequestFn = <
@ -180,10 +166,7 @@ type RequestFn = <
TResponseStyle extends ResponseStyle = 'fields',
>(
options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, 'method'> &
Pick<
Required<RequestOptions<TData, TResponseStyle, ThrowOnError>>,
'method'
>,
Pick<Required<RequestOptions<TData, TResponseStyle, ThrowOnError>>, 'method'>,
) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>;
type BuildUrlFn = <
@ -197,13 +180,7 @@ type BuildUrlFn = <
options: TData & Options<TData>,
) => string;
export type Client = CoreClient<
RequestFn,
Config,
MethodFn,
BuildUrlFn,
SseFn
> & {
export type Client = CoreClient<RequestFn, Config, MethodFn, BuildUrlFn, SseFn> & {
interceptors: Middleware<Request, Response, unknown, ResolvedRequestOptions>;
};

View file

@ -1,3 +1,4 @@
// @ts-nocheck
// This file is auto-generated by @hey-api/openapi-ts
import { getAuthToken } from '../core/auth.gen';
@ -65,9 +66,7 @@ export const createQuerySerializer = <T = unknown>({
/**
* Infers parseAs value from provided Content-Type header.
*/
export const getParseAs = (
contentType: string | null,
): Exclude<Config['parseAs'], 'auto'> => {
export const getParseAs = (contentType: string | null): Exclude<Config['parseAs'], 'auto'> => {
if (!contentType) {
// If no Content-Type header is provided, the best we can do is return the raw response body,
// which is effectively the same as the 'stream' option.
@ -80,10 +79,7 @@ export const getParseAs = (
return;
}
if (
cleanContent.startsWith('application/json') ||
cleanContent.endsWith('+json')
) {
if (cleanContent.startsWith('application/json') || cleanContent.endsWith('+json')) {
return 'json';
}
@ -92,9 +88,7 @@ export const getParseAs = (
}
if (
['application/', 'audio/', 'image/', 'video/'].some((type) =>
cleanContent.startsWith(type),
)
['application/', 'audio/', 'image/', 'video/'].some((type) => cleanContent.startsWith(type))
) {
return 'blob';
}
@ -125,14 +119,12 @@ const checkForExistence = (
return false;
};
export const setAuthParams = async ({
security,
...options
}: Pick<Required<RequestOptions>, 'security'> &
Pick<RequestOptions, 'auth' | 'query'> & {
export async function setAuthParams(
options: Pick<RequestOptions, 'auth' | 'query' | 'security'> & {
headers: Headers;
}) => {
for (const auth of security) {
},
): Promise<void> {
for (const auth of options.security ?? []) {
if (checkForExistence(options, auth.name)) {
continue;
}
@ -161,7 +153,7 @@ export const setAuthParams = async ({
break;
}
}
};
}
export const buildUrl: Client['buildUrl'] = (options) =>
getUrl({
@ -201,10 +193,7 @@ export const mergeHeaders = (
continue;
}
const iterator =
header instanceof Headers
? headersEntries(header)
: Object.entries(header);
const iterator = header instanceof Headers ? headersEntries(header) : Object.entries(header);
for (const [key, value] of iterator) {
if (value === null) {
@ -214,7 +203,7 @@ export const mergeHeaders = (
mergedHeaders.append(key, v as string);
}
} else if (value !== undefined) {
// assume object headers are meant to be JSON stringified, i.e. their
// assume object headers are meant to be JSON stringified, i.e., their
// content value in OpenAPI specification is 'application/json'
mergedHeaders.set(
key,
@ -228,15 +217,14 @@ export const mergeHeaders = (
type ErrInterceptor<Err, Res, Req, Options> = (
error: Err,
response: Res,
request: Req,
/** response may be undefined due to a network error where no response object is produced */
response: Res | undefined,
/** request may be undefined, because error may be from building the request object itself */
request: Req | undefined,
options: Options,
) => Err | Promise<Err>;
type ReqInterceptor<Req, Options> = (
request: Req,
options: Options,
) => Req | Promise<Req>;
type ReqInterceptor<Req, Options> = (request: Req, options: Options) => Req | Promise<Req>;
type ResInterceptor<Res, Req, Options> = (
response: Res,
@ -270,10 +258,7 @@ class Interceptors<Interceptor> {
return this.fns.indexOf(id);
}
update(
id: number | Interceptor,
fn: Interceptor,
): number | Interceptor | false {
update(id: number | Interceptor, fn: Interceptor): number | Interceptor | false {
const index = this.getInterceptorIndex(id);
if (this.fns[index]) {
this.fns[index] = fn;

View file

@ -1,3 +1,4 @@
// @ts-nocheck
// This file is auto-generated by @hey-api/openapi-ts
export type AuthToken = string | undefined;
@ -23,8 +24,7 @@ export const getAuthToken = async (
auth: Auth,
callback: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken,
): Promise<string | undefined> => {
const token =
typeof callback === 'function' ? await callback(auth) : callback;
const token = typeof callback === 'function' ? await callback(auth) : callback;
if (!token) {
return;

View file

@ -1,14 +1,11 @@
// @ts-nocheck
// This file is auto-generated by @hey-api/openapi-ts
import type {
ArrayStyle,
ObjectStyle,
SerializerOptions,
} from './pathSerializer.gen';
import type { ArrayStyle, ObjectStyle, SerializerOptions } from './pathSerializer.gen';
export type QuerySerializer = (query: Record<string, unknown>) => string;
export type BodySerializer = (body: any) => any;
export type BodySerializer = (body: unknown) => unknown;
type QuerySerializerOptionsObject = {
allowReserved?: boolean;
@ -24,11 +21,7 @@ export type QuerySerializerOptions = QuerySerializerOptionsObject & {
parameters?: Record<string, QuerySerializerOptionsObject>;
};
const serializeFormDataPair = (
data: FormData,
key: string,
value: unknown,
): void => {
const serializeFormDataPair = (data: FormData, key: string, value: unknown): void => {
if (typeof value === 'string' || value instanceof Blob) {
data.append(key, value);
} else if (value instanceof Date) {
@ -38,11 +31,7 @@ const serializeFormDataPair = (
}
};
const serializeUrlSearchParamsPair = (
data: URLSearchParams,
key: string,
value: unknown,
): void => {
const serializeUrlSearchParamsPair = (data: URLSearchParams, key: string, value: unknown): void => {
if (typeof value === 'string') {
data.append(key, value);
} else {
@ -51,12 +40,10 @@ const serializeUrlSearchParamsPair = (
};
export const formDataBodySerializer = {
bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>(
body: T,
): FormData => {
bodySerializer: (body: unknown): FormData => {
const data = new FormData();
Object.entries(body).forEach(([key, value]) => {
Object.entries(body as Record<string, unknown>).forEach(([key, value]) => {
if (value === undefined || value === null) {
return;
}
@ -72,19 +59,15 @@ export const formDataBodySerializer = {
};
export const jsonBodySerializer = {
bodySerializer: <T>(body: T): string =>
JSON.stringify(body, (_key, value) =>
typeof value === 'bigint' ? value.toString() : value,
),
bodySerializer: (body: unknown): string =>
JSON.stringify(body, (_key, value) => (typeof value === 'bigint' ? value.toString() : value)),
};
export const urlSearchParamsBodySerializer = {
bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>(
body: T,
): string => {
bodySerializer: (body: unknown): string => {
const data = new URLSearchParams();
Object.entries(body).forEach(([key, value]) => {
Object.entries(body as Record<string, unknown>).forEach(([key, value]) => {
if (value === undefined || value === null) {
return;
}

View file

@ -1,3 +1,4 @@
// @ts-nocheck
// This file is auto-generated by @hey-api/openapi-ts
type Slot = 'body' | 'headers' | 'path' | 'query';
@ -96,16 +97,13 @@ interface Params {
const stripEmptySlots = (params: Params) => {
for (const [slot, value] of Object.entries(params)) {
if (value && typeof value === 'object' && !Object.keys(value).length) {
if (value && typeof value === 'object' && !Array.isArray(value) && !Object.keys(value).length) {
delete params[slot as Slot];
}
}
};
export const buildClientParams = (
args: ReadonlyArray<unknown>,
fields: FieldsConfig,
) => {
export const buildClientParams = (args: ReadonlyArray<unknown>, fields: FieldsConfig) => {
const params: Params = {
body: {},
headers: {},
@ -148,15 +146,11 @@ export const buildClientParams = (
params[field.map] = value;
}
} else {
const extra = extraPrefixes.find(([prefix]) =>
key.startsWith(prefix),
);
const extra = extraPrefixes.find(([prefix]) => key.startsWith(prefix));
if (extra) {
const [prefix, slot] = extra;
(params[slot] as Record<string, unknown>)[
key.slice(prefix.length)
] = value;
(params[slot] as Record<string, unknown>)[key.slice(prefix.length)] = value;
} else if ('allowExtra' in config && config.allowExtra) {
for (const [slot, allowed] of Object.entries(config.allowExtra)) {
if (allowed) {

View file

@ -1,8 +1,7 @@
// @ts-nocheck
// This file is auto-generated by @hey-api/openapi-ts
interface SerializeOptions<T>
extends SerializePrimitiveOptions,
SerializerOptions<T> {}
interface SerializeOptions<T> extends SerializePrimitiveOptions, SerializerOptions<T> {}
interface SerializePrimitiveOptions {
allowReserved?: boolean;
@ -105,9 +104,7 @@ export const serializeArrayParam = ({
});
})
.join(separator);
return style === 'label' || style === 'matrix'
? separator + joinedValues
: joinedValues;
return style === 'label' || style === 'matrix' ? separator + joinedValues : joinedValues;
};
export const serializePrimitiveParam = ({
@ -146,11 +143,7 @@ export const serializeObjectParam = ({
if (style !== 'deepObject' && !explode) {
let values: string[] = [];
Object.entries(value).forEach(([key, v]) => {
values = [
...values,
key,
allowReserved ? (v as string) : encodeURIComponent(v as string),
];
values = [...values, key, allowReserved ? (v as string) : encodeURIComponent(v as string)];
});
const joinedValues = values.join(',');
switch (style) {
@ -175,7 +168,5 @@ export const serializeObjectParam = ({
}),
)
.join(separator);
return style === 'label' || style === 'matrix'
? separator + joinedValues
: joinedValues;
return style === 'label' || style === 'matrix' ? separator + joinedValues : joinedValues;
};

View file

@ -1,3 +1,4 @@
// @ts-nocheck
// This file is auto-generated by @hey-api/openapi-ts
/**
@ -15,11 +16,7 @@ export type JsonValue =
* Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes.
*/
export const queryKeyJsonReplacer = (_key: string, value: unknown) => {
if (
value === undefined ||
typeof value === 'function' ||
typeof value === 'symbol'
) {
if (value === undefined || typeof value === 'function' || typeof value === 'symbol') {
return undefined;
}
if (typeof value === 'bigint') {
@ -61,9 +58,7 @@ const isPlainObject = (value: unknown): value is Record<string, unknown> => {
* Turns URLSearchParams into a sorted JSON object for deterministic keys.
*/
const serializeSearchParams = (params: URLSearchParams): JsonValue => {
const entries = Array.from(params.entries()).sort(([a], [b]) =>
a.localeCompare(b),
);
const entries = Array.from(params.entries()).sort(([a], [b]) => a.localeCompare(b));
const result: Record<string, JsonValue> = {};
for (const [key, value] of entries) {
@ -86,26 +81,16 @@ const serializeSearchParams = (params: URLSearchParams): JsonValue => {
/**
* Normalizes any accepted value into a JSON-friendly shape for query keys.
*/
export const serializeQueryKeyValue = (
value: unknown,
): JsonValue | undefined => {
export const serializeQueryKeyValue = (value: unknown): JsonValue | undefined => {
if (value === null) {
return null;
}
if (
typeof value === 'string' ||
typeof value === 'number' ||
typeof value === 'boolean'
) {
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
return value;
}
if (
value === undefined ||
typeof value === 'function' ||
typeof value === 'symbol'
) {
if (value === undefined || typeof value === 'function' || typeof value === 'symbol') {
return undefined;
}
@ -121,10 +106,7 @@ export const serializeQueryKeyValue = (
return stringifyToJsonValue(value);
}
if (
typeof URLSearchParams !== 'undefined' &&
value instanceof URLSearchParams
) {
if (typeof URLSearchParams !== 'undefined' && value instanceof URLSearchParams) {
return serializeSearchParams(value);
}

View file

@ -1,11 +1,9 @@
// @ts-nocheck
// This file is auto-generated by @hey-api/openapi-ts
import type { Config } from './types.gen';
export type ServerSentEventsOptions<TData = unknown> = Omit<
RequestInit,
'method'
> &
export type ServerSentEventsOptions<TData = unknown> = Omit<RequestInit, 'method'> &
Pick<Config, 'method' | 'responseTransformer' | 'responseValidator'> & {
/**
* Fetch API implementation. You can use this option to provide a custom
@ -74,11 +72,7 @@ export interface StreamEvent<TData = unknown> {
retry?: number;
}
export type ServerSentEventsResult<
TData = unknown,
TReturn = void,
TNext = unknown,
> = {
export type ServerSentEventsResult<TData = unknown, TReturn = void, TNext = unknown> = {
stream: AsyncGenerator<
TData extends Record<string, unknown> ? TData[keyof TData] : TData,
TReturn,
@ -86,7 +80,7 @@ export type ServerSentEventsResult<
>;
};
export const createSseClient = <TData = unknown>({
export function createSseClient<TData = unknown>({
onRequest,
onSseError,
onSseEvent,
@ -98,12 +92,10 @@ export const createSseClient = <TData = unknown>({
sseSleepFn,
url,
...options
}: ServerSentEventsOptions): ServerSentEventsResult<TData> => {
}: ServerSentEventsOptions): ServerSentEventsResult<TData> {
let lastEventId: string | undefined;
const sleep =
sseSleepFn ??
((ms: number) => new Promise((resolve) => setTimeout(resolve, ms)));
const sleep = sseSleepFn ?? ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms)));
const createStream = async function* () {
let retryDelay: number = sseDefaultRetryDelay ?? 3000;
@ -141,16 +133,11 @@ export const createSseClient = <TData = unknown>({
const _fetch = options.fetch ?? globalThis.fetch;
const response = await _fetch(request);
if (!response.ok)
throw new Error(
`SSE failed: ${response.status} ${response.statusText}`,
);
if (!response.ok) throw new Error(`SSE failed: ${response.status} ${response.statusText}`);
if (!response.body) throw new Error('No body in SSE response');
const reader = response.body
.pipeThrough(new TextDecoderStream())
.getReader();
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
let buffer = '';
@ -169,6 +156,7 @@ export const createSseClient = <TData = unknown>({
const { done, value } = await reader.read();
if (done) break;
buffer += value;
buffer = buffer.replace(/\r\n?/g, '\n'); // normalize line endings
const chunks = buffer.split('\n\n');
buffer = chunks.pop() ?? '';
@ -186,10 +174,7 @@ export const createSseClient = <TData = unknown>({
} else if (line.startsWith('id:')) {
lastEventId = line.replace(/^id:\s*/, '');
} else if (line.startsWith('retry:')) {
const parsed = Number.parseInt(
line.replace(/^retry:\s*/, ''),
10,
);
const parsed = Number.parseInt(line.replace(/^retry:\s*/, ''), 10);
if (!Number.isNaN(parsed)) {
retryDelay = parsed;
}
@ -241,18 +226,12 @@ export const createSseClient = <TData = unknown>({
// connection failed or aborted; retry after delay
onSseError?.(error);
if (
sseMaxRetryAttempts !== undefined &&
attempt >= sseMaxRetryAttempts
) {
if (sseMaxRetryAttempts !== undefined && attempt >= sseMaxRetryAttempts) {
break; // stop after firing error
}
// exponential backoff: double retry each attempt, cap at 30s
const backoff = Math.min(
retryDelay * 2 ** (attempt - 1),
sseMaxRetryDelay ?? 30000,
);
const backoff = Math.min(retryDelay * 2 ** (attempt - 1), sseMaxRetryDelay ?? 30000);
await sleep(backoff);
}
}
@ -261,4 +240,4 @@ export const createSseClient = <TData = unknown>({
const stream = createStream();
return { stream };
};
}

View file

@ -1,11 +1,8 @@
// @ts-nocheck
// This file is auto-generated by @hey-api/openapi-ts
import type { Auth, AuthToken } from './auth.gen';
import type {
BodySerializer,
QuerySerializer,
QuerySerializerOptions,
} from './bodySerializer.gen';
import type { BodySerializer, QuerySerializer, QuerySerializerOptions } from './bodySerializer.gen';
export type HttpMethod =
| 'connect'
@ -34,9 +31,7 @@ export type Client<
setConfig: (config: Config) => Config;
} & {
[K in HttpMethod]: MethodFn;
} & ([SseFn] extends [never]
? { sse?: never }
: { sse: { [K in HttpMethod]: SseFn } });
} & ([SseFn] extends [never] ? { sse?: never } : { sse: { [K in HttpMethod]: SseFn } });
export interface Config {
/**
@ -59,13 +54,7 @@ export interface Config {
| RequestInit['headers']
| Record<
string,
| string
| number
| boolean
| (string | number | boolean)[]
| null
| undefined
| unknown
string | number | boolean | (string | number | boolean)[] | null | undefined | unknown
>;
/**
* The request method.
@ -92,7 +81,7 @@ export interface Config {
requestValidator?: (data: unknown) => Promise<unknown>;
/**
* A function transforming response data before it's returned. This is useful
* for post-processing data, e.g. converting ISO strings into Date objects.
* for post-processing data, e.g., converting ISO strings into Date objects.
*/
responseTransformer?: (data: unknown) => Promise<unknown>;
/**
@ -112,7 +101,5 @@ type IsExactlyNeverOrNeverUndefined<T> = [T] extends [never]
: false;
export type OmitNever<T extends Record<string, unknown>> = {
[K in keyof T as IsExactlyNeverOrNeverUndefined<T[K]> extends true
? never
: K]: T[K];
[K in keyof T as IsExactlyNeverOrNeverUndefined<T[K]> extends true ? never : K]: T[K];
};

View file

@ -1,3 +1,4 @@
// @ts-nocheck
// This file is auto-generated by @hey-api/openapi-ts
import type { BodySerializer, QuerySerializer } from './bodySerializer.gen';
@ -44,10 +45,7 @@ export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => {
}
if (Array.isArray(value)) {
url = url.replace(
match,
serializeArrayParam({ explode, name, style, value }),
);
url = url.replace(match, serializeArrayParam({ explode, name, style, value }));
continue;
}
@ -129,7 +127,7 @@ export function getValidRequestBody(options: {
return hasSerializedBody ? options.serializedBody : null;
}
// not all clients implement a serializedBody property (i.e. client-axios)
// not all clients implement a serializedBody property (i.e., client-axios)
return options.body !== '' ? options.body : null;
}

View file

@ -1,4 +1,318 @@
// @ts-nocheck
// This file is auto-generated by @hey-api/openapi-ts
export type * from './types.gen';
export * from './sdk.gen';
export {
browseFilesystem,
cancelDoctor,
createBackupSchedule,
createNotificationDestination,
createRepository,
createVolume,
deleteBackupSchedule,
deleteNotificationDestination,
deleteRepository,
deleteSnapshot,
deleteSnapshots,
deleteSsoInvitation,
deleteSsoProvider,
deleteUserAccount,
deleteVolume,
devPanelExec,
downloadResticPassword,
dumpSnapshot,
getAdminUsers,
getBackupProgress,
getBackupSchedule,
getBackupScheduleForVolume,
getDevPanel,
getMirrorCompatibility,
getMirrorSyncStatus,
getNotificationDestination,
getOrgMembers,
getPublicSsoProviders,
getRegistrationStatus,
getRepository,
getRepositoryStats,
getScheduleMirrors,
getScheduleNotifications,
getSnapshotDetails,
getSsoSettings,
getStatus,
getSystemInfo,
getUpdates,
getUserDeletionImpact,
getVolume,
healthCheckVolume,
listBackupSchedules,
listFiles,
listNotificationDestinations,
listRcloneRemotes,
listRepositories,
listSnapshotFiles,
listSnapshots,
listVolumes,
mountVolume,
type Options,
refreshRepositoryStats,
refreshSnapshots,
removeOrgMember,
reorderBackupSchedules,
restoreSnapshot,
runBackupNow,
runForget,
setRegistrationStatus,
startDoctor,
stopBackup,
syncMirror,
tagSnapshots,
testConnection,
testNotificationDestination,
unlockRepository,
unmountVolume,
updateBackupSchedule,
updateMemberRole,
updateNotificationDestination,
updateRepository,
updateScheduleMirrors,
updateScheduleNotifications,
updateSsoProviderAutoLinking,
updateVolume,
} from "./sdk.gen";
export type {
BrowseFilesystemData,
BrowseFilesystemResponse,
BrowseFilesystemResponses,
CancelDoctorData,
CancelDoctorErrors,
CancelDoctorResponse,
CancelDoctorResponses,
ClientOptions,
CreateBackupScheduleData,
CreateBackupScheduleResponse,
CreateBackupScheduleResponses,
CreateNotificationDestinationData,
CreateNotificationDestinationResponse,
CreateNotificationDestinationResponses,
CreateRepositoryData,
CreateRepositoryResponse,
CreateRepositoryResponses,
CreateVolumeData,
CreateVolumeResponse,
CreateVolumeResponses,
DeleteBackupScheduleData,
DeleteBackupScheduleResponse,
DeleteBackupScheduleResponses,
DeleteNotificationDestinationData,
DeleteNotificationDestinationErrors,
DeleteNotificationDestinationResponse,
DeleteNotificationDestinationResponses,
DeleteRepositoryData,
DeleteRepositoryResponse,
DeleteRepositoryResponses,
DeleteSnapshotData,
DeleteSnapshotResponse,
DeleteSnapshotResponses,
DeleteSnapshotsData,
DeleteSnapshotsResponse,
DeleteSnapshotsResponses,
DeleteSsoInvitationData,
DeleteSsoInvitationErrors,
DeleteSsoInvitationResponses,
DeleteSsoProviderData,
DeleteSsoProviderErrors,
DeleteSsoProviderResponses,
DeleteUserAccountData,
DeleteUserAccountErrors,
DeleteUserAccountResponses,
DeleteVolumeData,
DeleteVolumeResponse,
DeleteVolumeResponses,
DevPanelExecData,
DevPanelExecErrors,
DevPanelExecResponse,
DevPanelExecResponses,
DownloadResticPasswordData,
DownloadResticPasswordResponse,
DownloadResticPasswordResponses,
DumpSnapshotData,
DumpSnapshotResponse,
DumpSnapshotResponses,
GetAdminUsersData,
GetAdminUsersResponse,
GetAdminUsersResponses,
GetBackupProgressData,
GetBackupProgressResponse,
GetBackupProgressResponses,
GetBackupScheduleData,
GetBackupScheduleForVolumeData,
GetBackupScheduleForVolumeResponse,
GetBackupScheduleForVolumeResponses,
GetBackupScheduleResponse,
GetBackupScheduleResponses,
GetDevPanelData,
GetDevPanelResponse,
GetDevPanelResponses,
GetMirrorCompatibilityData,
GetMirrorCompatibilityResponse,
GetMirrorCompatibilityResponses,
GetMirrorSyncStatusData,
GetMirrorSyncStatusResponse,
GetMirrorSyncStatusResponses,
GetNotificationDestinationData,
GetNotificationDestinationErrors,
GetNotificationDestinationResponse,
GetNotificationDestinationResponses,
GetOrgMembersData,
GetOrgMembersResponse,
GetOrgMembersResponses,
GetPublicSsoProvidersData,
GetPublicSsoProvidersResponse,
GetPublicSsoProvidersResponses,
GetRegistrationStatusData,
GetRegistrationStatusResponse,
GetRegistrationStatusResponses,
GetRepositoryData,
GetRepositoryResponse,
GetRepositoryResponses,
GetRepositoryStatsData,
GetRepositoryStatsResponse,
GetRepositoryStatsResponses,
GetScheduleMirrorsData,
GetScheduleMirrorsResponse,
GetScheduleMirrorsResponses,
GetScheduleNotificationsData,
GetScheduleNotificationsResponse,
GetScheduleNotificationsResponses,
GetSnapshotDetailsData,
GetSnapshotDetailsResponse,
GetSnapshotDetailsResponses,
GetSsoSettingsData,
GetSsoSettingsResponse,
GetSsoSettingsResponses,
GetStatusData,
GetStatusResponse,
GetStatusResponses,
GetSystemInfoData,
GetSystemInfoResponse,
GetSystemInfoResponses,
GetUpdatesData,
GetUpdatesResponse,
GetUpdatesResponses,
GetUserDeletionImpactData,
GetUserDeletionImpactResponse,
GetUserDeletionImpactResponses,
GetVolumeData,
GetVolumeErrors,
GetVolumeResponse,
GetVolumeResponses,
HealthCheckVolumeData,
HealthCheckVolumeErrors,
HealthCheckVolumeResponse,
HealthCheckVolumeResponses,
ListBackupSchedulesData,
ListBackupSchedulesResponse,
ListBackupSchedulesResponses,
ListFilesData,
ListFilesResponse,
ListFilesResponses,
ListNotificationDestinationsData,
ListNotificationDestinationsResponse,
ListNotificationDestinationsResponses,
ListRcloneRemotesData,
ListRcloneRemotesResponse,
ListRcloneRemotesResponses,
ListRepositoriesData,
ListRepositoriesResponse,
ListRepositoriesResponses,
ListSnapshotFilesData,
ListSnapshotFilesResponse,
ListSnapshotFilesResponses,
ListSnapshotsData,
ListSnapshotsResponse,
ListSnapshotsResponses,
ListVolumesData,
ListVolumesResponse,
ListVolumesResponses,
MountVolumeData,
MountVolumeResponse,
MountVolumeResponses,
RefreshRepositoryStatsData,
RefreshRepositoryStatsResponse,
RefreshRepositoryStatsResponses,
RefreshSnapshotsData,
RefreshSnapshotsResponse,
RefreshSnapshotsResponses,
RemoveOrgMemberData,
RemoveOrgMemberErrors,
RemoveOrgMemberResponses,
ReorderBackupSchedulesData,
ReorderBackupSchedulesResponse,
ReorderBackupSchedulesResponses,
RestoreSnapshotData,
RestoreSnapshotResponse,
RestoreSnapshotResponses,
RunBackupNowData,
RunBackupNowResponse,
RunBackupNowResponses,
RunForgetData,
RunForgetResponse,
RunForgetResponses,
SetRegistrationStatusData,
SetRegistrationStatusResponse,
SetRegistrationStatusResponses,
StartDoctorData,
StartDoctorErrors,
StartDoctorResponse,
StartDoctorResponses,
StopBackupData,
StopBackupErrors,
StopBackupResponse,
StopBackupResponses,
SyncMirrorData,
SyncMirrorErrors,
SyncMirrorResponse,
SyncMirrorResponses,
TagSnapshotsData,
TagSnapshotsResponse,
TagSnapshotsResponses,
TestConnectionData,
TestConnectionResponse,
TestConnectionResponses,
TestNotificationDestinationData,
TestNotificationDestinationErrors,
TestNotificationDestinationResponse,
TestNotificationDestinationResponses,
UnlockRepositoryData,
UnlockRepositoryResponse,
UnlockRepositoryResponses,
UnmountVolumeData,
UnmountVolumeResponse,
UnmountVolumeResponses,
UpdateBackupScheduleData,
UpdateBackupScheduleResponse,
UpdateBackupScheduleResponses,
UpdateMemberRoleData,
UpdateMemberRoleErrors,
UpdateMemberRoleResponses,
UpdateNotificationDestinationData,
UpdateNotificationDestinationErrors,
UpdateNotificationDestinationResponse,
UpdateNotificationDestinationResponses,
UpdateRepositoryData,
UpdateRepositoryErrors,
UpdateRepositoryResponse,
UpdateRepositoryResponses,
UpdateScheduleMirrorsData,
UpdateScheduleMirrorsResponse,
UpdateScheduleMirrorsResponses,
UpdateScheduleNotificationsData,
UpdateScheduleNotificationsResponse,
UpdateScheduleNotificationsResponses,
UpdateSsoProviderAutoLinkingData,
UpdateSsoProviderAutoLinkingErrors,
UpdateSsoProviderAutoLinkingResponses,
UpdateVolumeData,
UpdateVolumeErrors,
UpdateVolumeResponse,
UpdateVolumeResponses,
} from "./types.gen";

View file

@ -1,10 +1,11 @@
// @ts-nocheck
// This file is auto-generated by @hey-api/openapi-ts
import type { Client, Options as Options2, TDataShape } from './client';
import { client } from './client.gen';
import type { BrowseFilesystemData, BrowseFilesystemResponses, ChangePasswordData, ChangePasswordResponses, CreateBackupScheduleData, CreateBackupScheduleResponses, CreateRepositoryData, CreateRepositoryResponses, CreateVolumeData, CreateVolumeResponses, DeleteBackupScheduleData, DeleteBackupScheduleResponses, DeleteRepositoryData, DeleteRepositoryResponses, DeleteSnapshotData, DeleteSnapshotResponses, DeleteVolumeData, DeleteVolumeResponses, DoctorRepositoryData, DoctorRepositoryResponses, DownloadResticPasswordData, DownloadResticPasswordResponses, GetBackupScheduleData, GetBackupScheduleForVolumeData, GetBackupScheduleForVolumeResponses, GetBackupScheduleResponses, GetContainersUsingVolumeData, GetContainersUsingVolumeErrors, GetContainersUsingVolumeResponses, GetMeData, GetMeResponses, GetRepositoryData, GetRepositoryResponses, GetSnapshotDetailsData, GetSnapshotDetailsResponses, GetStatusData, GetStatusResponses, GetSystemInfoData, GetSystemInfoResponses, GetVolumeData, GetVolumeErrors, GetVolumeResponses, HealthCheckVolumeData, HealthCheckVolumeErrors, HealthCheckVolumeResponses, ListBackupSchedulesData, ListBackupSchedulesResponses, ListFilesData, ListFilesResponses, ListRcloneRemotesData, ListRcloneRemotesResponses, ListRepositoriesData, ListRepositoriesResponses, ListSnapshotFilesData, ListSnapshotFilesResponses, ListSnapshotsData, ListSnapshotsResponses, ListVolumesData, ListVolumesResponses, LoginData, LoginResponses, LogoutData, LogoutResponses, MountVolumeData, MountVolumeResponses, RegisterData, RegisterResponses, RestoreSnapshotData, RestoreSnapshotResponses, RunBackupNowData, RunBackupNowResponses, RunForgetData, RunForgetResponses, StopBackupData, StopBackupErrors, StopBackupResponses, TestConnectionData, TestConnectionResponses, UnmountVolumeData, UnmountVolumeResponses, UpdateBackupScheduleData, UpdateBackupScheduleResponses, UpdateVolumeData, UpdateVolumeErrors, UpdateVolumeResponses } from './types.gen';
import type { BrowseFilesystemData, BrowseFilesystemResponses, CancelDoctorData, CancelDoctorErrors, CancelDoctorResponses, CreateBackupScheduleData, CreateBackupScheduleResponses, CreateNotificationDestinationData, CreateNotificationDestinationResponses, CreateRepositoryData, CreateRepositoryResponses, CreateVolumeData, CreateVolumeResponses, DeleteBackupScheduleData, DeleteBackupScheduleResponses, DeleteNotificationDestinationData, DeleteNotificationDestinationErrors, DeleteNotificationDestinationResponses, DeleteRepositoryData, DeleteRepositoryResponses, DeleteSnapshotData, DeleteSnapshotResponses, DeleteSnapshotsData, DeleteSnapshotsResponses, DeleteSsoInvitationData, DeleteSsoInvitationErrors, DeleteSsoInvitationResponses, DeleteSsoProviderData, DeleteSsoProviderErrors, DeleteSsoProviderResponses, DeleteUserAccountData, DeleteUserAccountErrors, DeleteUserAccountResponses, DeleteVolumeData, DeleteVolumeResponses, DevPanelExecData, DevPanelExecErrors, DevPanelExecResponse, DevPanelExecResponses, DownloadResticPasswordData, DownloadResticPasswordResponses, DumpSnapshotData, DumpSnapshotResponses, GetAdminUsersData, GetAdminUsersResponses, GetBackupProgressData, GetBackupProgressResponses, GetBackupScheduleData, GetBackupScheduleForVolumeData, GetBackupScheduleForVolumeResponses, GetBackupScheduleResponses, GetDevPanelData, GetDevPanelResponses, GetMirrorCompatibilityData, GetMirrorCompatibilityResponses, GetMirrorSyncStatusData, GetMirrorSyncStatusResponses, GetNotificationDestinationData, GetNotificationDestinationErrors, GetNotificationDestinationResponses, GetOrgMembersData, GetOrgMembersResponses, GetPublicSsoProvidersData, GetPublicSsoProvidersResponses, GetRegistrationStatusData, GetRegistrationStatusResponses, GetRepositoryData, GetRepositoryResponses, GetRepositoryStatsData, GetRepositoryStatsResponses, GetScheduleMirrorsData, GetScheduleMirrorsResponses, GetScheduleNotificationsData, GetScheduleNotificationsResponses, GetSnapshotDetailsData, GetSnapshotDetailsResponses, GetSsoSettingsData, GetSsoSettingsResponses, GetStatusData, GetStatusResponses, GetSystemInfoData, GetSystemInfoResponses, GetUpdatesData, GetUpdatesResponses, GetUserDeletionImpactData, GetUserDeletionImpactResponses, GetVolumeData, GetVolumeErrors, GetVolumeResponses, HealthCheckVolumeData, HealthCheckVolumeErrors, HealthCheckVolumeResponses, ListBackupSchedulesData, ListBackupSchedulesResponses, ListFilesData, ListFilesResponses, ListNotificationDestinationsData, ListNotificationDestinationsResponses, ListRcloneRemotesData, ListRcloneRemotesResponses, ListRepositoriesData, ListRepositoriesResponses, ListSnapshotFilesData, ListSnapshotFilesResponses, ListSnapshotsData, ListSnapshotsResponses, ListVolumesData, ListVolumesResponses, MountVolumeData, MountVolumeResponses, RefreshRepositoryStatsData, RefreshRepositoryStatsResponses, RefreshSnapshotsData, RefreshSnapshotsResponses, RemoveOrgMemberData, RemoveOrgMemberErrors, RemoveOrgMemberResponses, ReorderBackupSchedulesData, ReorderBackupSchedulesResponses, RestoreSnapshotData, RestoreSnapshotResponses, RunBackupNowData, RunBackupNowResponses, RunForgetData, RunForgetResponses, SetRegistrationStatusData, SetRegistrationStatusResponses, StartDoctorData, StartDoctorErrors, StartDoctorResponses, StopBackupData, StopBackupErrors, StopBackupResponses, SyncMirrorData, SyncMirrorErrors, SyncMirrorResponses, TagSnapshotsData, TagSnapshotsResponses, TestConnectionData, TestConnectionResponses, TestNotificationDestinationData, TestNotificationDestinationErrors, TestNotificationDestinationResponses, UnlockRepositoryData, UnlockRepositoryResponses, UnmountVolumeData, UnmountVolumeResponses, UpdateBackupScheduleData, UpdateBackupScheduleResponses, UpdateMemberRoleData, UpdateMemberRoleErrors, UpdateMemberRoleResponses, UpdateNotificationDestinationData, UpdateNotificationDestinationErrors, UpdateNotificationDestinationResponses, UpdateRepositoryData, UpdateRepositoryErrors, UpdateRepositoryResponses, UpdateScheduleMirrorsData, UpdateScheduleMirrorsResponses, UpdateScheduleNotificationsData, UpdateScheduleNotificationsResponses, UpdateSsoProviderAutoLinkingData, UpdateSsoProviderAutoLinkingErrors, UpdateSsoProviderAutoLinkingResponses, UpdateVolumeData, UpdateVolumeErrors, UpdateVolumeResponses } from './types.gen';
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = Options2<TData, ThrowOnError> & {
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean, TResponse = unknown> = Options2<TData, ThrowOnError, TResponse> & {
/**
* You can provide a client instance returned by `createClient()` instead of
* individual options. This might be also useful if you want to implement a
@ -18,446 +19,519 @@ export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends
meta?: Record<string, unknown>;
};
/**
* Register a new user
*/
export const register = <ThrowOnError extends boolean = false>(options?: Options<RegisterData, ThrowOnError>) => {
return (options?.client ?? client).post<RegisterResponses, unknown, ThrowOnError>({
url: '/api/v1/auth/register',
...options,
headers: {
'Content-Type': 'application/json',
...options?.headers
}
});
};
/**
* Login with username and password
*/
export const login = <ThrowOnError extends boolean = false>(options?: Options<LoginData, ThrowOnError>) => {
return (options?.client ?? client).post<LoginResponses, unknown, ThrowOnError>({
url: '/api/v1/auth/login',
...options,
headers: {
'Content-Type': 'application/json',
...options?.headers
}
});
};
/**
* Logout current user
*/
export const logout = <ThrowOnError extends boolean = false>(options?: Options<LogoutData, ThrowOnError>) => {
return (options?.client ?? client).post<LogoutResponses, unknown, ThrowOnError>({
url: '/api/v1/auth/logout',
...options
});
};
/**
* Get current authenticated user
*/
export const getMe = <ThrowOnError extends boolean = false>(options?: Options<GetMeData, ThrowOnError>) => {
return (options?.client ?? client).get<GetMeResponses, unknown, ThrowOnError>({
url: '/api/v1/auth/me',
...options
});
};
/**
* Get authentication system status
*/
export const getStatus = <ThrowOnError extends boolean = false>(options?: Options<GetStatusData, ThrowOnError>) => {
return (options?.client ?? client).get<GetStatusResponses, unknown, ThrowOnError>({
url: '/api/v1/auth/status',
...options
});
};
export const getStatus = <ThrowOnError extends boolean = false>(options?: Options<GetStatusData, ThrowOnError>) => (options?.client ?? client).get<GetStatusResponses, unknown, ThrowOnError>({ url: '/api/v1/auth/status', ...options });
/**
* Change current user password
* List admin users for settings management
*/
export const changePassword = <ThrowOnError extends boolean = false>(options?: Options<ChangePasswordData, ThrowOnError>) => {
return (options?.client ?? client).post<ChangePasswordResponses, unknown, ThrowOnError>({
url: '/api/v1/auth/change-password',
...options,
headers: {
'Content-Type': 'application/json',
...options?.headers
}
});
};
export const getAdminUsers = <ThrowOnError extends boolean = false>(options?: Options<GetAdminUsersData, ThrowOnError>) => (options?.client ?? client).get<GetAdminUsersResponses, unknown, ThrowOnError>({ url: '/api/v1/auth/admin-users', ...options });
/**
* Delete an account linked to a user
*/
export const deleteUserAccount = <ThrowOnError extends boolean = false>(options: Options<DeleteUserAccountData, ThrowOnError>) => (options.client ?? client).delete<DeleteUserAccountResponses, DeleteUserAccountErrors, ThrowOnError>({ url: '/api/v1/auth/admin-users/{userId}/accounts/{accountId}', ...options });
/**
* Get impact of deleting a user
*/
export const getUserDeletionImpact = <ThrowOnError extends boolean = false>(options: Options<GetUserDeletionImpactData, ThrowOnError>) => (options.client ?? client).get<GetUserDeletionImpactResponses, unknown, ThrowOnError>({ url: '/api/v1/auth/deletion-impact/{userId}', ...options });
/**
* Get members of the active organization
*/
export const getOrgMembers = <ThrowOnError extends boolean = false>(options?: Options<GetOrgMembersData, ThrowOnError>) => (options?.client ?? client).get<GetOrgMembersResponses, unknown, ThrowOnError>({ url: '/api/v1/auth/org-members', ...options });
/**
* Update a member's role in the active organization
*/
export const updateMemberRole = <ThrowOnError extends boolean = false>(options: Options<UpdateMemberRoleData, ThrowOnError>) => (options.client ?? client).patch<UpdateMemberRoleResponses, UpdateMemberRoleErrors, ThrowOnError>({
url: '/api/v1/auth/org-members/{memberId}/role',
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
/**
* Remove a member from the active organization
*/
export const removeOrgMember = <ThrowOnError extends boolean = false>(options: Options<RemoveOrgMemberData, ThrowOnError>) => (options.client ?? client).delete<RemoveOrgMemberResponses, RemoveOrgMemberErrors, ThrowOnError>({ url: '/api/v1/auth/org-members/{memberId}', ...options });
/**
* Get public SSO providers for the instance
*/
export const getPublicSsoProviders = <ThrowOnError extends boolean = false>(options?: Options<GetPublicSsoProvidersData, ThrowOnError>) => (options?.client ?? client).get<GetPublicSsoProvidersResponses, unknown, ThrowOnError>({ url: '/api/v1/auth/sso-providers', ...options });
/**
* Get SSO providers and invitations for the active organization
*/
export const getSsoSettings = <ThrowOnError extends boolean = false>(options?: Options<GetSsoSettingsData, ThrowOnError>) => (options?.client ?? client).get<GetSsoSettingsResponses, unknown, ThrowOnError>({ url: '/api/v1/auth/sso-settings', ...options });
/**
* Delete an SSO provider
*/
export const deleteSsoProvider = <ThrowOnError extends boolean = false>(options: Options<DeleteSsoProviderData, ThrowOnError>) => (options.client ?? client).delete<DeleteSsoProviderResponses, DeleteSsoProviderErrors, ThrowOnError>({ url: '/api/v1/auth/sso-providers/{providerId}', ...options });
/**
* Update whether SSO sign-in can auto-link existing accounts by email
*/
export const updateSsoProviderAutoLinking = <ThrowOnError extends boolean = false>(options: Options<UpdateSsoProviderAutoLinkingData, ThrowOnError>) => (options.client ?? client).patch<UpdateSsoProviderAutoLinkingResponses, UpdateSsoProviderAutoLinkingErrors, ThrowOnError>({
url: '/api/v1/auth/sso-providers/{providerId}/auto-linking',
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
/**
* Delete an SSO invitation
*/
export const deleteSsoInvitation = <ThrowOnError extends boolean = false>(options: Options<DeleteSsoInvitationData, ThrowOnError>) => (options.client ?? client).delete<DeleteSsoInvitationResponses, DeleteSsoInvitationErrors, ThrowOnError>({ url: '/api/v1/auth/sso-invitations/{invitationId}', ...options });
/**
* List all volumes
*/
export const listVolumes = <ThrowOnError extends boolean = false>(options?: Options<ListVolumesData, ThrowOnError>) => {
return (options?.client ?? client).get<ListVolumesResponses, unknown, ThrowOnError>({
url: '/api/v1/volumes',
...options
});
};
export const listVolumes = <ThrowOnError extends boolean = false>(options?: Options<ListVolumesData, ThrowOnError>) => (options?.client ?? client).get<ListVolumesResponses, unknown, ThrowOnError>({ url: '/api/v1/volumes', ...options });
/**
* Create a new volume
*/
export const createVolume = <ThrowOnError extends boolean = false>(options?: Options<CreateVolumeData, ThrowOnError>) => {
return (options?.client ?? client).post<CreateVolumeResponses, unknown, ThrowOnError>({
url: '/api/v1/volumes',
...options,
headers: {
'Content-Type': 'application/json',
...options?.headers
}
});
};
export const createVolume = <ThrowOnError extends boolean = false>(options: Options<CreateVolumeData, ThrowOnError>) => (options.client ?? client).post<CreateVolumeResponses, unknown, ThrowOnError>({
url: '/api/v1/volumes',
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
/**
* Test connection to backend
*/
export const testConnection = <ThrowOnError extends boolean = false>(options?: Options<TestConnectionData, ThrowOnError>) => {
return (options?.client ?? client).post<TestConnectionResponses, unknown, ThrowOnError>({
url: '/api/v1/volumes/test-connection',
...options,
headers: {
'Content-Type': 'application/json',
...options?.headers
}
});
};
export const testConnection = <ThrowOnError extends boolean = false>(options: Options<TestConnectionData, ThrowOnError>) => (options.client ?? client).post<TestConnectionResponses, unknown, ThrowOnError>({
url: '/api/v1/volumes/test-connection',
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
/**
* Delete a volume
*/
export const deleteVolume = <ThrowOnError extends boolean = false>(options: Options<DeleteVolumeData, ThrowOnError>) => {
return (options.client ?? client).delete<DeleteVolumeResponses, unknown, ThrowOnError>({
url: '/api/v1/volumes/{name}',
...options
});
};
export const deleteVolume = <ThrowOnError extends boolean = false>(options: Options<DeleteVolumeData, ThrowOnError>) => (options.client ?? client).delete<DeleteVolumeResponses, unknown, ThrowOnError>({ url: '/api/v1/volumes/{shortId}', ...options });
/**
* Get a volume by name
*/
export const getVolume = <ThrowOnError extends boolean = false>(options: Options<GetVolumeData, ThrowOnError>) => {
return (options.client ?? client).get<GetVolumeResponses, GetVolumeErrors, ThrowOnError>({
url: '/api/v1/volumes/{name}',
...options
});
};
export const getVolume = <ThrowOnError extends boolean = false>(options: Options<GetVolumeData, ThrowOnError>) => (options.client ?? client).get<GetVolumeResponses, GetVolumeErrors, ThrowOnError>({ url: '/api/v1/volumes/{shortId}', ...options });
/**
* Update a volume's configuration
*/
export const updateVolume = <ThrowOnError extends boolean = false>(options: Options<UpdateVolumeData, ThrowOnError>) => {
return (options.client ?? client).put<UpdateVolumeResponses, UpdateVolumeErrors, ThrowOnError>({
url: '/api/v1/volumes/{name}',
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
};
/**
* Get containers using a volume by name
*/
export const getContainersUsingVolume = <ThrowOnError extends boolean = false>(options: Options<GetContainersUsingVolumeData, ThrowOnError>) => {
return (options.client ?? client).get<GetContainersUsingVolumeResponses, GetContainersUsingVolumeErrors, ThrowOnError>({
url: '/api/v1/volumes/{name}/containers',
...options
});
};
export const updateVolume = <ThrowOnError extends boolean = false>(options: Options<UpdateVolumeData, ThrowOnError>) => (options.client ?? client).put<UpdateVolumeResponses, UpdateVolumeErrors, ThrowOnError>({
url: '/api/v1/volumes/{shortId}',
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
/**
* Mount a volume
*/
export const mountVolume = <ThrowOnError extends boolean = false>(options: Options<MountVolumeData, ThrowOnError>) => {
return (options.client ?? client).post<MountVolumeResponses, unknown, ThrowOnError>({
url: '/api/v1/volumes/{name}/mount',
...options
});
};
export const mountVolume = <ThrowOnError extends boolean = false>(options: Options<MountVolumeData, ThrowOnError>) => (options.client ?? client).post<MountVolumeResponses, unknown, ThrowOnError>({ url: '/api/v1/volumes/{shortId}/mount', ...options });
/**
* Unmount a volume
*/
export const unmountVolume = <ThrowOnError extends boolean = false>(options: Options<UnmountVolumeData, ThrowOnError>) => {
return (options.client ?? client).post<UnmountVolumeResponses, unknown, ThrowOnError>({
url: '/api/v1/volumes/{name}/unmount',
...options
});
};
export const unmountVolume = <ThrowOnError extends boolean = false>(options: Options<UnmountVolumeData, ThrowOnError>) => (options.client ?? client).post<UnmountVolumeResponses, unknown, ThrowOnError>({ url: '/api/v1/volumes/{shortId}/unmount', ...options });
/**
* Perform a health check on a volume
*/
export const healthCheckVolume = <ThrowOnError extends boolean = false>(options: Options<HealthCheckVolumeData, ThrowOnError>) => {
return (options.client ?? client).post<HealthCheckVolumeResponses, HealthCheckVolumeErrors, ThrowOnError>({
url: '/api/v1/volumes/{name}/health-check',
...options
});
};
export const healthCheckVolume = <ThrowOnError extends boolean = false>(options: Options<HealthCheckVolumeData, ThrowOnError>) => (options.client ?? client).post<HealthCheckVolumeResponses, HealthCheckVolumeErrors, ThrowOnError>({ url: '/api/v1/volumes/{shortId}/health-check', ...options });
/**
* List files in a volume directory
*/
export const listFiles = <ThrowOnError extends boolean = false>(options: Options<ListFilesData, ThrowOnError>) => {
return (options.client ?? client).get<ListFilesResponses, unknown, ThrowOnError>({
url: '/api/v1/volumes/{name}/files',
...options
});
};
export const listFiles = <ThrowOnError extends boolean = false>(options: Options<ListFilesData, ThrowOnError>) => (options.client ?? client).get<ListFilesResponses, unknown, ThrowOnError>({ url: '/api/v1/volumes/{shortId}/files', ...options });
/**
* Browse directories on the host filesystem
*/
export const browseFilesystem = <ThrowOnError extends boolean = false>(options?: Options<BrowseFilesystemData, ThrowOnError>) => {
return (options?.client ?? client).get<BrowseFilesystemResponses, unknown, ThrowOnError>({
url: '/api/v1/volumes/filesystem/browse',
...options
});
};
export const browseFilesystem = <ThrowOnError extends boolean = false>(options?: Options<BrowseFilesystemData, ThrowOnError>) => (options?.client ?? client).get<BrowseFilesystemResponses, unknown, ThrowOnError>({ url: '/api/v1/volumes/filesystem/browse', ...options });
/**
* List all repositories
*/
export const listRepositories = <ThrowOnError extends boolean = false>(options?: Options<ListRepositoriesData, ThrowOnError>) => {
return (options?.client ?? client).get<ListRepositoriesResponses, unknown, ThrowOnError>({
url: '/api/v1/repositories',
...options
});
};
export const listRepositories = <ThrowOnError extends boolean = false>(options?: Options<ListRepositoriesData, ThrowOnError>) => (options?.client ?? client).get<ListRepositoriesResponses, unknown, ThrowOnError>({ url: '/api/v1/repositories', ...options });
/**
* Create a new restic repository
*/
export const createRepository = <ThrowOnError extends boolean = false>(options?: Options<CreateRepositoryData, ThrowOnError>) => {
return (options?.client ?? client).post<CreateRepositoryResponses, unknown, ThrowOnError>({
url: '/api/v1/repositories',
...options,
headers: {
'Content-Type': 'application/json',
...options?.headers
}
});
};
export const createRepository = <ThrowOnError extends boolean = false>(options: Options<CreateRepositoryData, ThrowOnError>) => (options.client ?? client).post<CreateRepositoryResponses, unknown, ThrowOnError>({
url: '/api/v1/repositories',
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
/**
* List all configured rclone remotes on the host system
*/
export const listRcloneRemotes = <ThrowOnError extends boolean = false>(options?: Options<ListRcloneRemotesData, ThrowOnError>) => {
return (options?.client ?? client).get<ListRcloneRemotesResponses, unknown, ThrowOnError>({
url: '/api/v1/repositories/rclone-remotes',
...options
});
};
export const listRcloneRemotes = <ThrowOnError extends boolean = false>(options?: Options<ListRcloneRemotesData, ThrowOnError>) => (options?.client ?? client).get<ListRcloneRemotesResponses, unknown, ThrowOnError>({ url: '/api/v1/repositories/rclone-remotes', ...options });
/**
* Delete a repository
*/
export const deleteRepository = <ThrowOnError extends boolean = false>(options: Options<DeleteRepositoryData, ThrowOnError>) => {
return (options.client ?? client).delete<DeleteRepositoryResponses, unknown, ThrowOnError>({
url: '/api/v1/repositories/{name}',
...options
});
};
export const deleteRepository = <ThrowOnError extends boolean = false>(options: Options<DeleteRepositoryData, ThrowOnError>) => (options.client ?? client).delete<DeleteRepositoryResponses, unknown, ThrowOnError>({ url: '/api/v1/repositories/{shortId}', ...options });
/**
* Get a single repository by name
* Get a single repository by ID
*/
export const getRepository = <ThrowOnError extends boolean = false>(options: Options<GetRepositoryData, ThrowOnError>) => {
return (options.client ?? client).get<GetRepositoryResponses, unknown, ThrowOnError>({
url: '/api/v1/repositories/{name}',
...options
});
};
export const getRepository = <ThrowOnError extends boolean = false>(options: Options<GetRepositoryData, ThrowOnError>) => (options.client ?? client).get<GetRepositoryResponses, unknown, ThrowOnError>({ url: '/api/v1/repositories/{shortId}', ...options });
/**
* Update a repository's name or settings
*/
export const updateRepository = <ThrowOnError extends boolean = false>(options: Options<UpdateRepositoryData, ThrowOnError>) => (options.client ?? client).patch<UpdateRepositoryResponses, UpdateRepositoryErrors, ThrowOnError>({
url: '/api/v1/repositories/{shortId}',
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
/**
* Get repository storage and compression statistics
*/
export const getRepositoryStats = <ThrowOnError extends boolean = false>(options: Options<GetRepositoryStatsData, ThrowOnError>) => (options.client ?? client).get<GetRepositoryStatsResponses, unknown, ThrowOnError>({ url: '/api/v1/repositories/{shortId}/stats', ...options });
/**
* Refresh repository storage and compression statistics
*/
export const refreshRepositoryStats = <ThrowOnError extends boolean = false>(options: Options<RefreshRepositoryStatsData, ThrowOnError>) => (options.client ?? client).post<RefreshRepositoryStatsResponses, unknown, ThrowOnError>({ url: '/api/v1/repositories/{shortId}/stats/refresh', ...options });
/**
* Delete multiple snapshots from a repository
*/
export const deleteSnapshots = <ThrowOnError extends boolean = false>(options: Options<DeleteSnapshotsData, ThrowOnError>) => (options.client ?? client).delete<DeleteSnapshotsResponses, unknown, ThrowOnError>({
url: '/api/v1/repositories/{shortId}/snapshots',
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
/**
* List all snapshots in a repository
*/
export const listSnapshots = <ThrowOnError extends boolean = false>(options: Options<ListSnapshotsData, ThrowOnError>) => {
return (options.client ?? client).get<ListSnapshotsResponses, unknown, ThrowOnError>({
url: '/api/v1/repositories/{name}/snapshots',
...options
});
};
export const listSnapshots = <ThrowOnError extends boolean = false>(options: Options<ListSnapshotsData, ThrowOnError>) => (options.client ?? client).get<ListSnapshotsResponses, unknown, ThrowOnError>({ url: '/api/v1/repositories/{shortId}/snapshots', ...options });
/**
* Clear snapshot cache and force refresh from repository
*/
export const refreshSnapshots = <ThrowOnError extends boolean = false>(options: Options<RefreshSnapshotsData, ThrowOnError>) => (options.client ?? client).post<RefreshSnapshotsResponses, unknown, ThrowOnError>({ url: '/api/v1/repositories/{shortId}/snapshots/refresh', ...options });
/**
* Delete a specific snapshot from a repository
*/
export const deleteSnapshot = <ThrowOnError extends boolean = false>(options: Options<DeleteSnapshotData, ThrowOnError>) => {
return (options.client ?? client).delete<DeleteSnapshotResponses, unknown, ThrowOnError>({
url: '/api/v1/repositories/{name}/snapshots/{snapshotId}',
...options
});
};
export const deleteSnapshot = <ThrowOnError extends boolean = false>(options: Options<DeleteSnapshotData, ThrowOnError>) => (options.client ?? client).delete<DeleteSnapshotResponses, unknown, ThrowOnError>({ url: '/api/v1/repositories/{shortId}/snapshots/{snapshotId}', ...options });
/**
* Get details of a specific snapshot
*/
export const getSnapshotDetails = <ThrowOnError extends boolean = false>(options: Options<GetSnapshotDetailsData, ThrowOnError>) => {
return (options.client ?? client).get<GetSnapshotDetailsResponses, unknown, ThrowOnError>({
url: '/api/v1/repositories/{name}/snapshots/{snapshotId}',
...options
});
};
export const getSnapshotDetails = <ThrowOnError extends boolean = false>(options: Options<GetSnapshotDetailsData, ThrowOnError>) => (options.client ?? client).get<GetSnapshotDetailsResponses, unknown, ThrowOnError>({ url: '/api/v1/repositories/{shortId}/snapshots/{snapshotId}', ...options });
/**
* List files and directories in a snapshot
*/
export const listSnapshotFiles = <ThrowOnError extends boolean = false>(options: Options<ListSnapshotFilesData, ThrowOnError>) => {
return (options.client ?? client).get<ListSnapshotFilesResponses, unknown, ThrowOnError>({
url: '/api/v1/repositories/{name}/snapshots/{snapshotId}/files',
...options
});
};
export const listSnapshotFiles = <ThrowOnError extends boolean = false>(options: Options<ListSnapshotFilesData, ThrowOnError>) => (options.client ?? client).get<ListSnapshotFilesResponses, unknown, ThrowOnError>({ url: '/api/v1/repositories/{shortId}/snapshots/{snapshotId}/files', ...options });
/**
* Download a snapshot path as a tar archive (folders) or raw file stream (single files)
*/
export const dumpSnapshot = <ThrowOnError extends boolean = false>(options: Options<DumpSnapshotData, ThrowOnError>) => (options.client ?? client).get<DumpSnapshotResponses, unknown, ThrowOnError>({ url: '/api/v1/repositories/{shortId}/snapshots/{snapshotId}/dump', ...options });
/**
* Restore a snapshot to a target path on the filesystem
*/
export const restoreSnapshot = <ThrowOnError extends boolean = false>(options: Options<RestoreSnapshotData, ThrowOnError>) => {
return (options.client ?? client).post<RestoreSnapshotResponses, unknown, ThrowOnError>({
url: '/api/v1/repositories/{name}/restore',
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
};
export const restoreSnapshot = <ThrowOnError extends boolean = false>(options: Options<RestoreSnapshotData, ThrowOnError>) => (options.client ?? client).post<RestoreSnapshotResponses, unknown, ThrowOnError>({
url: '/api/v1/repositories/{shortId}/restore',
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
/**
* Run doctor operations on a repository to fix common issues (unlock, check, repair index). Use this when the repository is locked or has errors.
* Cancel a running doctor operation on a repository
*/
export const doctorRepository = <ThrowOnError extends boolean = false>(options: Options<DoctorRepositoryData, ThrowOnError>) => {
return (options.client ?? client).post<DoctorRepositoryResponses, unknown, ThrowOnError>({
url: '/api/v1/repositories/{name}/doctor',
...options
});
};
export const cancelDoctor = <ThrowOnError extends boolean = false>(options: Options<CancelDoctorData, ThrowOnError>) => (options.client ?? client).delete<CancelDoctorResponses, CancelDoctorErrors, ThrowOnError>({ url: '/api/v1/repositories/{shortId}/doctor', ...options });
/**
* Start an asynchronous doctor operation on a repository to fix common issues (unlock, check, repair index). The operation runs in the background and sends results via SSE events.
*/
export const startDoctor = <ThrowOnError extends boolean = false>(options: Options<StartDoctorData, ThrowOnError>) => (options.client ?? client).post<StartDoctorResponses, StartDoctorErrors, ThrowOnError>({ url: '/api/v1/repositories/{shortId}/doctor', ...options });
/**
* Unlock a repository by removing all stale locks
*/
export const unlockRepository = <ThrowOnError extends boolean = false>(options: Options<UnlockRepositoryData, ThrowOnError>) => (options.client ?? client).post<UnlockRepositoryResponses, unknown, ThrowOnError>({ url: '/api/v1/repositories/{shortId}/unlock', ...options });
/**
* Tag multiple snapshots in a repository
*/
export const tagSnapshots = <ThrowOnError extends boolean = false>(options: Options<TagSnapshotsData, ThrowOnError>) => (options.client ?? client).post<TagSnapshotsResponses, unknown, ThrowOnError>({
url: '/api/v1/repositories/{shortId}/snapshots/tag',
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
/**
* Execute a restic command against a repository (dev panel only)
*/
export const devPanelExec = <ThrowOnError extends boolean = false>(options: Options<DevPanelExecData, ThrowOnError, DevPanelExecResponse>) => (options.client ?? client).sse.post<DevPanelExecResponses, DevPanelExecErrors, ThrowOnError>({
url: '/api/v1/repositories/{shortId}/exec',
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
/**
* List all backup schedules
*/
export const listBackupSchedules = <ThrowOnError extends boolean = false>(options?: Options<ListBackupSchedulesData, ThrowOnError>) => {
return (options?.client ?? client).get<ListBackupSchedulesResponses, unknown, ThrowOnError>({
url: '/api/v1/backups',
...options
});
};
export const listBackupSchedules = <ThrowOnError extends boolean = false>(options?: Options<ListBackupSchedulesData, ThrowOnError>) => (options?.client ?? client).get<ListBackupSchedulesResponses, unknown, ThrowOnError>({ url: '/api/v1/backups', ...options });
/**
* Create a new backup schedule for a volume
*/
export const createBackupSchedule = <ThrowOnError extends boolean = false>(options?: Options<CreateBackupScheduleData, ThrowOnError>) => {
return (options?.client ?? client).post<CreateBackupScheduleResponses, unknown, ThrowOnError>({
url: '/api/v1/backups',
...options,
headers: {
'Content-Type': 'application/json',
...options?.headers
}
});
};
export const createBackupSchedule = <ThrowOnError extends boolean = false>(options: Options<CreateBackupScheduleData, ThrowOnError>) => (options.client ?? client).post<CreateBackupScheduleResponses, unknown, ThrowOnError>({
url: '/api/v1/backups',
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
/**
* Delete a backup schedule
*/
export const deleteBackupSchedule = <ThrowOnError extends boolean = false>(options: Options<DeleteBackupScheduleData, ThrowOnError>) => {
return (options.client ?? client).delete<DeleteBackupScheduleResponses, unknown, ThrowOnError>({
url: '/api/v1/backups/{scheduleId}',
...options
});
};
export const deleteBackupSchedule = <ThrowOnError extends boolean = false>(options: Options<DeleteBackupScheduleData, ThrowOnError>) => (options.client ?? client).delete<DeleteBackupScheduleResponses, unknown, ThrowOnError>({ url: '/api/v1/backups/{shortId}', ...options });
/**
* Get a backup schedule by ID
*/
export const getBackupSchedule = <ThrowOnError extends boolean = false>(options: Options<GetBackupScheduleData, ThrowOnError>) => {
return (options.client ?? client).get<GetBackupScheduleResponses, unknown, ThrowOnError>({
url: '/api/v1/backups/{scheduleId}',
...options
});
};
export const getBackupSchedule = <ThrowOnError extends boolean = false>(options: Options<GetBackupScheduleData, ThrowOnError>) => (options.client ?? client).get<GetBackupScheduleResponses, unknown, ThrowOnError>({ url: '/api/v1/backups/{shortId}', ...options });
/**
* Update a backup schedule
*/
export const updateBackupSchedule = <ThrowOnError extends boolean = false>(options: Options<UpdateBackupScheduleData, ThrowOnError>) => {
return (options.client ?? client).patch<UpdateBackupScheduleResponses, unknown, ThrowOnError>({
url: '/api/v1/backups/{scheduleId}',
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
};
export const updateBackupSchedule = <ThrowOnError extends boolean = false>(options: Options<UpdateBackupScheduleData, ThrowOnError>) => (options.client ?? client).patch<UpdateBackupScheduleResponses, unknown, ThrowOnError>({
url: '/api/v1/backups/{shortId}',
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
/**
* Get a backup schedule for a specific volume
*/
export const getBackupScheduleForVolume = <ThrowOnError extends boolean = false>(options: Options<GetBackupScheduleForVolumeData, ThrowOnError>) => {
return (options.client ?? client).get<GetBackupScheduleForVolumeResponses, unknown, ThrowOnError>({
url: '/api/v1/backups/volume/{volumeId}',
...options
});
};
export const getBackupScheduleForVolume = <ThrowOnError extends boolean = false>(options: Options<GetBackupScheduleForVolumeData, ThrowOnError>) => (options.client ?? client).get<GetBackupScheduleForVolumeResponses, unknown, ThrowOnError>({ url: '/api/v1/backups/volume/{volumeShortId}', ...options });
/**
* Trigger a backup immediately for a schedule
*/
export const runBackupNow = <ThrowOnError extends boolean = false>(options: Options<RunBackupNowData, ThrowOnError>) => {
return (options.client ?? client).post<RunBackupNowResponses, unknown, ThrowOnError>({
url: '/api/v1/backups/{scheduleId}/run',
...options
});
};
export const runBackupNow = <ThrowOnError extends boolean = false>(options: Options<RunBackupNowData, ThrowOnError>) => (options.client ?? client).post<RunBackupNowResponses, unknown, ThrowOnError>({ url: '/api/v1/backups/{shortId}/run', ...options });
/**
* Stop a backup that is currently in progress
*/
export const stopBackup = <ThrowOnError extends boolean = false>(options: Options<StopBackupData, ThrowOnError>) => {
return (options.client ?? client).post<StopBackupResponses, StopBackupErrors, ThrowOnError>({
url: '/api/v1/backups/{scheduleId}/stop',
...options
});
};
export const stopBackup = <ThrowOnError extends boolean = false>(options: Options<StopBackupData, ThrowOnError>) => (options.client ?? client).post<StopBackupResponses, StopBackupErrors, ThrowOnError>({ url: '/api/v1/backups/{shortId}/stop', ...options });
/**
* Manually apply retention policy to clean up old snapshots
*/
export const runForget = <ThrowOnError extends boolean = false>(options: Options<RunForgetData, ThrowOnError>) => {
return (options.client ?? client).post<RunForgetResponses, unknown, ThrowOnError>({
url: '/api/v1/backups/{scheduleId}/forget',
...options
});
};
export const runForget = <ThrowOnError extends boolean = false>(options: Options<RunForgetData, ThrowOnError>) => (options.client ?? client).post<RunForgetResponses, unknown, ThrowOnError>({ url: '/api/v1/backups/{shortId}/forget', ...options });
/**
* Get notification assignments for a backup schedule
*/
export const getScheduleNotifications = <ThrowOnError extends boolean = false>(options: Options<GetScheduleNotificationsData, ThrowOnError>) => (options.client ?? client).get<GetScheduleNotificationsResponses, unknown, ThrowOnError>({ url: '/api/v1/backups/{shortId}/notifications', ...options });
/**
* Update notification assignments for a backup schedule
*/
export const updateScheduleNotifications = <ThrowOnError extends boolean = false>(options: Options<UpdateScheduleNotificationsData, ThrowOnError>) => (options.client ?? client).put<UpdateScheduleNotificationsResponses, unknown, ThrowOnError>({
url: '/api/v1/backups/{shortId}/notifications',
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
/**
* Get mirror repository assignments for a backup schedule
*/
export const getScheduleMirrors = <ThrowOnError extends boolean = false>(options: Options<GetScheduleMirrorsData, ThrowOnError>) => (options.client ?? client).get<GetScheduleMirrorsResponses, unknown, ThrowOnError>({ url: '/api/v1/backups/{shortId}/mirrors', ...options });
/**
* Update mirror repository assignments for a backup schedule
*/
export const updateScheduleMirrors = <ThrowOnError extends boolean = false>(options: Options<UpdateScheduleMirrorsData, ThrowOnError>) => (options.client ?? client).put<UpdateScheduleMirrorsResponses, unknown, ThrowOnError>({
url: '/api/v1/backups/{shortId}/mirrors',
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
/**
* Get sync status for a specific mirror, including missing snapshots
*/
export const getMirrorSyncStatus = <ThrowOnError extends boolean = false>(options: Options<GetMirrorSyncStatusData, ThrowOnError>) => (options.client ?? client).get<GetMirrorSyncStatusResponses, unknown, ThrowOnError>({ url: '/api/v1/backups/{shortId}/mirrors/{mirrorShortId}/status', ...options });
/**
* Sync selected snapshots to a specific mirror repository
*/
export const syncMirror = <ThrowOnError extends boolean = false>(options: Options<SyncMirrorData, ThrowOnError>) => (options.client ?? client).post<SyncMirrorResponses, SyncMirrorErrors, ThrowOnError>({
url: '/api/v1/backups/{shortId}/mirrors/{mirrorShortId}/sync',
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
/**
* Get mirror compatibility info for all repositories relative to a backup schedule's primary repository
*/
export const getMirrorCompatibility = <ThrowOnError extends boolean = false>(options: Options<GetMirrorCompatibilityData, ThrowOnError>) => (options.client ?? client).get<GetMirrorCompatibilityResponses, unknown, ThrowOnError>({ url: '/api/v1/backups/{shortId}/mirrors/compatibility', ...options });
/**
* Reorder backup schedules by providing an array of schedule short IDs in the desired order
*/
export const reorderBackupSchedules = <ThrowOnError extends boolean = false>(options: Options<ReorderBackupSchedulesData, ThrowOnError>) => (options.client ?? client).post<ReorderBackupSchedulesResponses, unknown, ThrowOnError>({
url: '/api/v1/backups/reorder',
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
/**
* Get the last known progress for a currently running backup. Returns null if no progress has been reported yet.
*/
export const getBackupProgress = <ThrowOnError extends boolean = false>(options: Options<GetBackupProgressData, ThrowOnError>) => (options.client ?? client).get<GetBackupProgressResponses, unknown, ThrowOnError>({ url: '/api/v1/backups/{shortId}/progress', ...options });
/**
* List all notification destinations
*/
export const listNotificationDestinations = <ThrowOnError extends boolean = false>(options?: Options<ListNotificationDestinationsData, ThrowOnError>) => (options?.client ?? client).get<ListNotificationDestinationsResponses, unknown, ThrowOnError>({ url: '/api/v1/notifications/destinations', ...options });
/**
* Create a new notification destination
*/
export const createNotificationDestination = <ThrowOnError extends boolean = false>(options: Options<CreateNotificationDestinationData, ThrowOnError>) => (options.client ?? client).post<CreateNotificationDestinationResponses, unknown, ThrowOnError>({
url: '/api/v1/notifications/destinations',
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
/**
* Delete a notification destination
*/
export const deleteNotificationDestination = <ThrowOnError extends boolean = false>(options: Options<DeleteNotificationDestinationData, ThrowOnError>) => (options.client ?? client).delete<DeleteNotificationDestinationResponses, DeleteNotificationDestinationErrors, ThrowOnError>({ url: '/api/v1/notifications/destinations/{id}', ...options });
/**
* Get a notification destination by ID
*/
export const getNotificationDestination = <ThrowOnError extends boolean = false>(options: Options<GetNotificationDestinationData, ThrowOnError>) => (options.client ?? client).get<GetNotificationDestinationResponses, GetNotificationDestinationErrors, ThrowOnError>({ url: '/api/v1/notifications/destinations/{id}', ...options });
/**
* Update a notification destination
*/
export const updateNotificationDestination = <ThrowOnError extends boolean = false>(options: Options<UpdateNotificationDestinationData, ThrowOnError>) => (options.client ?? client).patch<UpdateNotificationDestinationResponses, UpdateNotificationDestinationErrors, ThrowOnError>({
url: '/api/v1/notifications/destinations/{id}',
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
/**
* Test a notification destination by sending a test message
*/
export const testNotificationDestination = <ThrowOnError extends boolean = false>(options: Options<TestNotificationDestinationData, ThrowOnError>) => (options.client ?? client).post<TestNotificationDestinationResponses, TestNotificationDestinationErrors, ThrowOnError>({ url: '/api/v1/notifications/destinations/{id}/test', ...options });
/**
* Get system information including available capabilities
*/
export const getSystemInfo = <ThrowOnError extends boolean = false>(options?: Options<GetSystemInfoData, ThrowOnError>) => {
return (options?.client ?? client).get<GetSystemInfoResponses, unknown, ThrowOnError>({
url: '/api/v1/system/info',
...options
});
};
export const getSystemInfo = <ThrowOnError extends boolean = false>(options?: Options<GetSystemInfoData, ThrowOnError>) => (options?.client ?? client).get<GetSystemInfoResponses, unknown, ThrowOnError>({ url: '/api/v1/system/info', ...options });
/**
* Download the Restic password file for backup recovery. Requires password re-authentication.
* Check for application updates from GitHub
*/
export const downloadResticPassword = <ThrowOnError extends boolean = false>(options?: Options<DownloadResticPasswordData, ThrowOnError>) => {
return (options?.client ?? client).post<DownloadResticPasswordResponses, unknown, ThrowOnError>({
url: '/api/v1/system/restic-password',
...options,
headers: {
'Content-Type': 'application/json',
...options?.headers
}
});
};
export const getUpdates = <ThrowOnError extends boolean = false>(options?: Options<GetUpdatesData, ThrowOnError>) => (options?.client ?? client).get<GetUpdatesResponses, unknown, ThrowOnError>({ url: '/api/v1/system/updates', ...options });
/**
* Get the current registration status for new users
*/
export const getRegistrationStatus = <ThrowOnError extends boolean = false>(options?: Options<GetRegistrationStatusData, ThrowOnError>) => (options?.client ?? client).get<GetRegistrationStatusResponses, unknown, ThrowOnError>({ url: '/api/v1/system/registration-status', ...options });
/**
* Update the registration status for new users. Requires global admin role.
*/
export const setRegistrationStatus = <ThrowOnError extends boolean = false>(options: Options<SetRegistrationStatusData, ThrowOnError>) => (options.client ?? client).put<SetRegistrationStatusResponses, unknown, ThrowOnError>({
url: '/api/v1/system/registration-status',
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
/**
* Download the organization's Restic password for backup recovery. Requires organization owner or admin role and password re-authentication.
*/
export const downloadResticPassword = <ThrowOnError extends boolean = false>(options: Options<DownloadResticPasswordData, ThrowOnError>) => (options.client ?? client).post<DownloadResticPasswordResponses, unknown, ThrowOnError>({
url: '/api/v1/system/restic-password',
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
/**
* Get the dev panel status
*/
export const getDevPanel = <ThrowOnError extends boolean = false>(options?: Options<GetDevPanelData, ThrowOnError>) => (options?.client ?? client).get<GetDevPanelResponses, unknown, ThrowOnError>({ url: '/api/v1/system/dev-panel', ...options });

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,276 @@
/** biome-ignore-all lint/style/noNonNullAssertion: Testing file - non-null assertions are acceptable here */
import { afterEach, describe, expect, test } from "vitest";
import { cleanup, render, screen, fireEvent, within } from "@testing-library/react";
import { useState } from "react";
import { FileTree, type FileEntry } from "../file-tree";
const getCheckboxFor = (name: string) => {
const row = screen.getByRole("button", { name });
return within(row).getByRole("checkbox");
};
const FileTreeSelection = ({
files,
initialSelectedPaths = [],
expandedFolders,
}: {
files: FileEntry[];
initialSelectedPaths?: string[];
expandedFolders?: Set<string>;
}) => {
const [selectedPaths, setSelectedPaths] = useState(() => new Set(initialSelectedPaths));
return (
<>
<FileTree
files={files}
withCheckboxes={true}
selectedPaths={selectedPaths}
onSelectionChange={setSelectedPaths}
expandedFolders={expandedFolders}
/>
<output aria-label="Selected paths">{JSON.stringify(Array.from(selectedPaths).sort())}</output>
</>
);
};
const getSelectedPaths = () => {
const selectedPaths = screen.getByLabelText("Selected paths").textContent;
return JSON.parse(selectedPaths ?? "[]") as string[];
};
afterEach(() => {
cleanup();
});
describe("FileTree Pagination", () => {
const testFiles: FileEntry[] = [
{ name: "root", path: "/root", type: "folder" },
{ name: "file1", path: "/root/file1", type: "file" },
{ name: "file2", path: "/root/file2", type: "file" },
];
test("shows load more button when hasMore is true", () => {
render(
<FileTree
files={testFiles}
expandedFolders={new Set(["/root"])}
getFolderPagination={(path) => {
if (path === "/root") {
return { hasMore: true, isLoadingMore: false };
}
return { hasMore: false, isLoadingMore: false };
}}
/>,
);
expect(screen.getByText("Load more files")).toBeTruthy();
});
test("does not show load more button when hasMore is false", () => {
render(
<FileTree
files={testFiles}
expandedFolders={new Set(["/root"])}
getFolderPagination={() => ({ hasMore: false, isLoadingMore: false })}
/>,
);
expect(screen.queryByText("Load more files")).toBeNull();
});
test("calls onLoadMore with folder path when load more button is clicked", () => {
let loadMoreCalled = false;
let loadMorePath = "";
render(
<FileTree
files={testFiles}
expandedFolders={new Set(["/root"])}
getFolderPagination={(path) => {
if (path === "/root") {
return { hasMore: true, isLoadingMore: false };
}
return { hasMore: false, isLoadingMore: false };
}}
onLoadMore={(path) => {
loadMoreCalled = true;
loadMorePath = path;
}}
/>,
);
const loadMoreButton = screen.getByText("Load more files");
fireEvent.click(loadMoreButton);
expect(loadMoreCalled).toBe(true);
expect(loadMorePath).toBe("/root");
});
test("shows loading state when isLoadingMore is true", () => {
render(
<FileTree
files={testFiles}
expandedFolders={new Set(["/root"])}
getFolderPagination={(path) => {
if (path === "/root") {
return { hasMore: true, isLoadingMore: true };
}
return { hasMore: false, isLoadingMore: false };
}}
/>,
);
expect(screen.getByText("Loading more...")).toBeTruthy();
});
test("shows load more button for root-level files when root has more", () => {
const rootFiles: FileEntry[] = [
{ name: "file1", path: "/file1", type: "file" },
{ name: "file2", path: "/file2", type: "file" },
];
render(
<FileTree
files={rootFiles}
getFolderPagination={(path) => {
if (path === "/") {
return { hasMore: true, isLoadingMore: false };
}
return { hasMore: false, isLoadingMore: false };
}}
/>,
);
expect(screen.getByText("Load more files")).toBeTruthy();
});
test("load more button appears for nested folders with hasMore", () => {
const nestedFiles: FileEntry[] = [
{ name: "root", path: "/root", type: "folder" },
{ name: "child", path: "/root/child", type: "folder" },
{ name: "file1", path: "/root/child/file1", type: "file" },
];
render(
<FileTree
files={nestedFiles}
expandedFolders={new Set(["/root", "/root/child"])}
getFolderPagination={(path) => {
if (path === "/root/child") {
return { hasMore: true, isLoadingMore: false };
}
return { hasMore: false, isLoadingMore: false };
}}
onLoadMore={() => {}}
/>,
);
expect(screen.getByText("Load more files")).toBeTruthy();
});
test("load more button does not appear when folder is collapsed", () => {
render(
<FileTree
files={testFiles}
expandedFolders={new Set([])}
getFolderPagination={(path) => {
if (path === "/root") {
return { hasMore: true, isLoadingMore: false };
}
return { hasMore: false, isLoadingMore: false };
}}
/>,
);
expect(screen.queryByText("Load more files")).toBeNull();
});
test("renders missing ancestor folders for nested paths", () => {
render(
<FileTree
files={[
{ name: "subdir", path: "/project/subdir", type: "folder" },
{ name: "file1", path: "/project/subdir/file1", type: "file" },
]}
/>,
);
expect(screen.getByRole("button", { name: "project" })).toBeTruthy();
});
});
describe("FileTree Selection Logic", () => {
const testFiles: FileEntry[] = [
{ name: "root", path: "/root", type: "folder" },
{ name: "photos", path: "/root/photos", type: "folder" },
{ name: "backups", path: "/root/photos/backups", type: "folder" },
{ name: "library", path: "/root/photos/library", type: "folder" },
{ name: "profile", path: "/root/photos/profile", type: "folder" },
{ name: "upload", path: "/root/photos/upload", type: "folder" },
];
test("selecting a folder simplifies to parent if it's the only child", () => {
render(<FileTreeSelection files={testFiles} expandedFolders={new Set(testFiles.map((f) => f.path))} />);
fireEvent.click(getCheckboxFor("photos"));
expect(getSelectedPaths()).toEqual(["/root"]);
});
test("unselecting a child removes the parent from selection", () => {
render(
<FileTreeSelection
files={testFiles}
initialSelectedPaths={["/root"]}
expandedFolders={new Set(testFiles.map((f) => f.path))}
/>,
);
fireEvent.click(getCheckboxFor("library"));
expect(getSelectedPaths()).toEqual(["/root/photos/backups", "/root/photos/profile", "/root/photos/upload"]);
});
test("recursive simplification when all children are selected", () => {
render(<FileTreeSelection files={testFiles} expandedFolders={new Set(testFiles.map((f) => f.path))} />);
const children = ["backups", "library", "profile", "upload"];
for (const name of children) {
fireEvent.click(getCheckboxFor(name));
}
expect(getSelectedPaths()).toEqual(["/root"]);
});
test("does not simplify to parent if not all children are selected", () => {
const multipleFiles: FileEntry[] = [
{ name: "root", path: "/root", type: "folder" },
{ name: "child1", path: "/root/child1", type: "folder" },
{ name: "child2", path: "/root/child2", type: "folder" },
];
render(<FileTreeSelection files={multipleFiles} expandedFolders={new Set(multipleFiles.map((f) => f.path))} />);
fireEvent.click(getCheckboxFor("child1"));
expect(getSelectedPaths()).toEqual(["/root/child1"]);
});
test("simplifies existing deep paths when parent is selected", () => {
const files: FileEntry[] = [
{ name: "hello", path: "/hello", type: "folder" },
{ name: "hello_prev", path: "/hello_prev", type: "folder" },
{ name: "service", path: "/service", type: "folder" },
];
render(
<FileTreeSelection files={files} initialSelectedPaths={["/hello", "/hello_prev", "/service/app/data/upload"]} />,
);
fireEvent.click(getCheckboxFor("service"));
expect(getSelectedPaths()).toEqual(["/hello", "/hello_prev", "/service"]);
});
});

View file

@ -0,0 +1,155 @@
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { HttpResponse, http, server } from "~/test/msw/server";
import { cleanup, render, screen, userEvent, waitFor, within } from "~/test/test-utils";
import { fromAny } from "@total-typescript/shoehorn";
vi.mock("@tanstack/react-router", async (importOriginal) => {
const actual = await importOriginal<typeof import("@tanstack/react-router")>();
return {
...actual,
useNavigate: (() => vi.fn(async () => {})) as typeof actual.useNavigate,
};
});
import { RestoreForm } from "../restore-form";
class MockEventSource {
addEventListener() {}
close() {}
onerror: ((event: Event) => void) | null = null;
constructor(public url: string) {}
}
const originalEventSource = globalThis.EventSource;
beforeEach(() => {
globalThis.EventSource = MockEventSource as unknown as typeof EventSource;
});
afterEach(() => {
globalThis.EventSource = originalEventSource;
cleanup();
});
describe("RestoreForm", () => {
test("restores the selected ancestor folder path from a broader display root", async () => {
let restoreRequestBody: unknown;
server.use(
http.get("/api/v1/repositories/:shortId/snapshots/:snapshotId/files", () => {
return HttpResponse.json({
files: [
{ name: "subdir", path: "/mnt/project/subdir", type: "dir" },
{ name: "deep.tx", path: "/mnt/project/subdir/deep.tx", type: "file" },
],
});
}),
http.post("/api/v1/repositories/:shortId/restore", async ({ request }) => {
restoreRequestBody = await request.json();
return HttpResponse.json({
success: true,
message: "Snapshot restored successfully",
filesRestored: 1,
filesSkipped: 0,
});
}),
);
render(
<RestoreForm
repository={fromAny({ shortId: "repo-1", name: "Repo 1" })}
snapshotId="snap-1"
returnPath="/repositories/repo-1/snap-1"
queryBasePath="/mnt/project/subdir"
displayBasePath="/mnt"
/>,
);
const row = await screen.findByRole("button", { name: "project" });
await userEvent.click(within(row).getByRole("checkbox"));
await userEvent.click(screen.getByRole("button", { name: "Restore 1 item" }));
await waitFor(() => {
expect(restoreRequestBody).toEqual({
snapshotId: "snap-1",
include: ["/mnt/project"],
selectedItemKind: "dir",
overwrite: "always",
});
});
});
test("restores the selected full path when the display root is unrelated", async () => {
let restoreRequestBody: unknown;
server.use(
http.get("/api/v1/repositories/:shortId/snapshots/:snapshotId/files", () => {
return HttpResponse.json({
files: [
{ name: "project", path: "/mnt/project", type: "dir" },
{ name: "a.txt", path: "/mnt/project/a.txt", type: "file" },
],
});
}),
http.post("/api/v1/repositories/:shortId/restore", async ({ request }) => {
restoreRequestBody = await request.json();
return HttpResponse.json({
success: true,
message: "Snapshot restored successfully",
filesRestored: 1,
filesSkipped: 0,
});
}),
http.get("/api/v1/volumes/filesystem/browse", () => {
return HttpResponse.json({
path: "/",
directories: [{ name: "restore-target", path: "/restore-target", type: "dir" }],
});
}),
);
render(
<RestoreForm
repository={fromAny({ shortId: "repo-1", name: "Repo 1" })}
snapshotId="snap-1"
returnPath="/repositories/repo-1/snap-1"
queryBasePath="/mnt/project"
displayBasePath="/other/root"
/>,
);
expect(
screen.getByText(
"This snapshot was created from source paths that do not match this Zerobyte server or the current linked volume. Restoring to the original location is unavailable. Restore it to a custom location, or download it instead.",
),
).toBeTruthy();
expect(screen.getByRole("button", { name: "Original location" }).hasAttribute("disabled")).toBe(true);
expect(screen.getByRole("button", { name: "Restore All" }).hasAttribute("disabled")).toBe(true);
await userEvent.click(screen.getByRole("button", { name: "Change" }));
await userEvent.click(await screen.findByRole("button", { name: "restore-target" }));
await waitFor(() => {
expect(screen.getByRole("button", { name: "Restore All" }).hasAttribute("disabled")).toBe(false);
});
const row = await screen.findByRole("button", { name: "mnt" });
await userEvent.click(within(row).getByRole("checkbox"));
await waitFor(() => {
expect(screen.getByRole("button", { name: "Restore 1 item" }).hasAttribute("disabled")).toBe(false);
});
await userEvent.click(screen.getByRole("button", { name: "Restore 1 item" }));
await waitFor(() => {
expect(restoreRequestBody).toEqual({
snapshotId: "snap-1",
include: ["/mnt"],
selectedItemKind: "dir",
targetPath: "/restore-target",
overwrite: "always",
});
});
});
});

View file

@ -1,4 +1,5 @@
import { Link, useMatches, type UIMatch } from "react-router";
import { Fragment } from "react";
import { useMatches, Link } from "@tanstack/react-router";
import {
Breadcrumb,
BreadcrumbItem,
@ -13,38 +14,35 @@ export interface BreadcrumbItemData {
href?: string;
}
interface RouteHandle {
breadcrumb?: (match: UIMatch) => BreadcrumbItemData[] | null;
}
type BreadcrumbFunction = (match: ReturnType<typeof useMatches>[number]) => BreadcrumbItemData[] | null;
export function AppBreadcrumb() {
const matches = useMatches();
// Find the last match with a breadcrumb handler
const lastMatchWithBreadcrumb = [...matches].reverse().find((match) => {
const handle = match.handle as RouteHandle | undefined;
return handle?.breadcrumb;
const breadcrumbFn = match.staticData?.breadcrumb as BreadcrumbFunction | undefined;
return breadcrumbFn;
});
if (!lastMatchWithBreadcrumb) {
return null;
}
const handle = lastMatchWithBreadcrumb.handle as RouteHandle;
const breadcrumbs = handle.breadcrumb?.(lastMatchWithBreadcrumb);
const breadcrumbFn = lastMatchWithBreadcrumb.staticData?.breadcrumb as BreadcrumbFunction;
const breadcrumbs = breadcrumbFn?.(lastMatchWithBreadcrumb);
if (!breadcrumbs || breadcrumbs.length === 0) {
return null;
}
return (
<Breadcrumb>
<Breadcrumb className="min-w-0">
<BreadcrumbList>
{breadcrumbs.map((breadcrumb, index) => {
const isLast = index === breadcrumbs.length - 1;
return (
<div key={`${breadcrumb.label}-${index}`} className="contents">
<Fragment key={`${breadcrumb.label}-${index}`}>
<BreadcrumbItem>
{isLast || !breadcrumb.href ? (
<BreadcrumbPage>{breadcrumb.label}</BreadcrumbPage>
@ -55,7 +53,7 @@ export function AppBreadcrumb() {
)}
</BreadcrumbItem>
{!isLast && <BreadcrumbSeparator />}
</div>
</Fragment>
);
})}
</BreadcrumbList>

View file

@ -1,5 +1,5 @@
import { CalendarClock, Database, HardDrive, Mountain, Settings } from "lucide-react";
import { Link, NavLink } from "react-router";
import { Bell, CalendarClock, Database, HardDrive, Settings, ShieldCheck } from "lucide-react";
import { useState } from "react";
import {
Sidebar,
SidebarContent,
@ -10,11 +10,17 @@ import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarSeparator,
useSidebar,
} from "~/client/components/ui/sidebar";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "~/client/components/ui/tooltip";
import { HoverCard, HoverCardContent, HoverCardTrigger } from "~/client/components/ui/hover-card";
import { cn } from "~/client/lib/utils";
import { APP_VERSION } from "~/client/lib/version";
import { APP_VERSION, RCLONE_VERSION, RESTIC_VERSION, SHOUTRRR_VERSION } from "~/client/lib/version";
import { useUpdates } from "~/client/hooks/use-updates";
import { ReleaseNotesDialog } from "./release-notes-dialog";
import { OrganizationSwitcher } from "./organization-switcher";
import { Link } from "@tanstack/react-router";
const items = [
{
@ -32,6 +38,11 @@ const items = [
url: "/backups",
icon: CalendarClock,
},
{
title: "Notifications",
url: "/notifications",
icon: Bell,
},
{
title: "Settings",
url: "/settings",
@ -39,20 +50,34 @@ const items = [
},
];
export function AppSidebar() {
const { state } = useSidebar();
type Props = {
isInstanceAdmin: boolean;
};
export function AppSidebar({ isInstanceAdmin }: Props) {
const { state, isMobile, setOpenMobile } = useSidebar();
const { updates, hasUpdate } = useUpdates();
const [showReleaseNotes, setShowReleaseNotes] = useState(false);
const isCollapsed = state === "collapsed";
const displayVersion = APP_VERSION.startsWith("v") || APP_VERSION === "dev" ? APP_VERSION : `v${APP_VERSION}`;
const releaseUrl =
APP_VERSION === "dev"
? "https://github.com/nicotsx/zerobyte"
: `https://github.com/nicotsx/zerobyte/releases/tag/${displayVersion}`;
return (
<Sidebar variant="inset" collapsible="icon" className="p-0">
<SidebarHeader className="bg-card-header border-b border-border/50 hidden md:flex h-[65px] flex-row items-center p-4">
<SidebarHeader className="bg-card-header border-b border-border/80 dark:border-border/50 hidden md:flex h-16.25 flex-row items-center p-4">
<Link to="/volumes" className="flex items-center gap-3 font-semibold pl-2">
<Mountain className="size-5 text-strong-accent" />
<img src="/images/zerobyte.png" alt="Zerobyte Logo" className={cn("h-8 w-8 shrink-0 object-contain -ml-2")} />
<span
className={cn("text-base transition-all duration-200", {
"opacity-0 w-0 overflow-hidden ": state === "collapsed",
className={cn("text-base transition-all duration-200 -ml-1", {
"opacity-0 w-0 overflow-hidden ": isCollapsed,
})}
>
Ironmount
Zerobyte
</span>
</Link>
</SidebarHeader>
@ -65,18 +90,43 @@ export function AppSidebar() {
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<SidebarMenuButton asChild>
<NavLink to={item.url}>
<SidebarMenuButton asChild className="relative overflow-hidden">
<Link
to={item.url}
onClick={() => isMobile && setOpenMobile(false)}
activeProps={{ className: "bg-strong-accent/10" }}
className="w-full flex items-center gap-2"
>
{({ isActive }) => (
<>
<item.icon className={cn({ "text-strong-accent": isActive })} />
<span className={cn({ "text-strong-accent": isActive })}>{item.title}</span>
{isActive && (
<div
className={cn("absolute left-0 top-0 h-full w-0.75 bg-strong-accent mr-2", {
hidden: isCollapsed,
})}
/>
)}
<item.icon
className={cn("transition-all duration-200", {
"text-strong-accent": isActive,
"ml-1": isActive && !isCollapsed,
"text-muted-foreground": !isActive,
})}
/>
<span
className={cn({
"text-foreground font-medium": isActive,
"text-muted-foreground": !isActive,
})}
>
{item.title}
</span>
</>
)}
</NavLink>
</Link>
</SidebarMenuButton>
</TooltipTrigger>
<TooltipContent side="right" className={cn({ hidden: state !== "collapsed" })}>
<TooltipContent side="right" className={cn({ hidden: !isCollapsed })}>
<p>{item.title}</p>
</TooltipContent>
</Tooltip>
@ -86,15 +136,104 @@ export function AppSidebar() {
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
{isInstanceAdmin && (
<>
<SidebarSeparator />
<SidebarGroup>
<SidebarGroupContent>
<SidebarMenu>
<SidebarMenuItem>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<SidebarMenuButton asChild className="relative overflow-hidden">
<Link
to="/admin"
onClick={() => isMobile && setOpenMobile(false)}
activeProps={{ className: "bg-strong-accent/10" }}
className="w-full flex items-center gap-2"
>
{({ isActive }) => (
<>
{isActive && (
<div
className={cn("absolute left-0 top-0 h-full w-0.75 bg-strong-accent mr-2", {
hidden: isCollapsed,
})}
/>
)}
<ShieldCheck
className={cn("transition-all duration-200", {
"text-strong-accent": isActive,
"ml-1": isActive && !isCollapsed,
"text-muted-foreground": !isActive,
})}
/>
<span
className={cn({
"text-foreground font-medium": isActive,
"text-muted-foreground": !isActive,
})}
>
Administration
</span>
</>
)}
</Link>
</SidebarMenuButton>
</TooltipTrigger>
<TooltipContent side="right" className={cn({ hidden: !isCollapsed })}>
<p>Administration</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</>
)}
</SidebarContent>
<SidebarFooter className="p-4 border-r border-t border-border/50">
<div
className={cn("text-xs text-muted-foreground transition-all duration-200", {
"opacity-0 w-0 overflow-hidden": state === "collapsed",
})}
>
{APP_VERSION}
<SidebarFooter className="p-4 border-r border-border/80 dark:border-border/50">
<OrganizationSwitcher />
<div className="flex items-center justify-between gap-2">
<HoverCard openDelay={200}>
<HoverCardTrigger asChild>
<a
href={releaseUrl}
target="_blank"
rel="noopener noreferrer"
className={cn("text-xs text-muted-foreground hover:text-foreground", {
"opacity-0 w-0 overflow-hidden": state === "collapsed",
})}
>
{displayVersion}
</a>
</HoverCardTrigger>
<HoverCardContent side="top" align="start" className="w-fit p-3">
<div className="flex flex-col gap-2">
<div className="grid grid-cols-[auto_1fr] gap-x-3 gap-y-1 text-[11px]">
<span className="text-muted-foreground">Restic:</span>
<span className="font-mono">{RESTIC_VERSION}</span>
<span className="text-muted-foreground">Rclone:</span>
<span className="font-mono">{RCLONE_VERSION}</span>
<span className="text-muted-foreground">Shoutrrr:</span>
<span className="font-mono">{SHOUTRRR_VERSION}</span>
</div>
</div>
</HoverCardContent>
</HoverCard>
{hasUpdate && state !== "collapsed" && (
<button
type="button"
onClick={() => setShowReleaseNotes(true)}
className="text-[10px] font-medium text-destructive hover:underline cursor-pointer"
>
Update available
</button>
)}
</div>
<ReleaseNotesDialog open={showReleaseNotes} onOpenChange={setShowReleaseNotes} updates={updates} />
</SidebarFooter>
</Sidebar>
);

View file

@ -1,4 +1,3 @@
import { Mountain } from "lucide-react";
import type { ReactNode } from "react";
type AuthLayoutProps = {
@ -13,8 +12,8 @@ export function AuthLayout({ title, description, children }: AuthLayoutProps) {
<div className="flex flex-1 items-center justify-center bg-background p-8">
<div className="w-full max-w-md space-y-8">
<div className="flex items-center gap-3">
<Mountain className="size-5 text-strong-accent" />
<span className="text-lg font-semibold">Ironmount</span>
<img src="/images/zerobyte.png" alt="Zerobyte Logo" className="h-5 w-5 object-contain" />
<span className="text-lg font-semibold">Zerobyte</span>
</div>
<div className="space-y-2">

View file

@ -0,0 +1,84 @@
import { Card, CardContent } from "~/client/components/ui/card";
import { ByteSize } from "~/client/components/bytes-size";
import { useRootLoaderData } from "~/client/hooks/use-root-loader-data";
import type { ResticSnapshotSummaryDto } from "@zerobyte/core/restic";
import { formatDuration } from "~/utils/utils";
type Props = {
summary?: ResticSnapshotSummaryDto | null;
};
const getDurationLabel = (start: string, end: string) => {
const startMs = new Date(start).getTime();
const endMs = new Date(end).getTime();
if (!Number.isFinite(startMs) || !Number.isFinite(endMs) || endMs < startMs) return "-";
return formatDuration(Math.round((endMs - startMs) / 1000));
};
export const BackupSummaryCard = ({ summary }: Props) => {
const { locale } = useRootLoaderData();
if (!summary) return null;
const formatCount = (value: number) => value.toLocaleString(locale);
const durationLabel = getDurationLabel(summary.backup_start, summary.backup_end);
const topStats = [
{
label: "Data added",
value: <ByteSize bytes={summary.data_added} base={1024} />,
},
{
label: "Data stored",
value: <ByteSize bytes={summary.data_added_packed ?? 0} base={1024} />,
},
{
label: "Files processed",
value: formatCount(summary.total_files_processed),
},
{
label: "Bytes processed",
value: <ByteSize bytes={summary.total_bytes_processed} base={1024} />,
},
{
label: "Duration",
value: durationLabel,
},
];
const detailStats = [
{ label: "New files", value: formatCount(summary.files_new) },
{ label: "Changed files", value: formatCount(summary.files_changed) },
{ label: "Unmodified files", value: formatCount(summary.files_unmodified) },
{ label: "New dirs", value: formatCount(summary.dirs_new) },
{ label: "Changed dirs", value: formatCount(summary.dirs_changed) },
{ label: "Unmodified dirs", value: formatCount(summary.dirs_unmodified) },
{ label: "Data blobs", value: formatCount(summary.data_blobs) },
{ label: "Tree blobs", value: formatCount(summary.tree_blobs) },
];
return (
<Card className="p-4">
<CardContent className="px-4">
<div className="grid gap-6 grid-cols-2 lg:grid-cols-5">
{topStats.map((stat) => (
<div key={stat.label} className="flex flex-col gap-1">
<span className="text-[11px] uppercase tracking-wide text-muted-foreground">{stat.label}</span>
<span className="text-sm font-semibold text-foreground">{stat.value}</span>
</div>
))}
</div>
<div className="mt-4 border-t border-border/60 pt-3">
<div className="grid gap-x-6 gap-y-2 grid-cols-2 lg:grid-cols-4">
{detailStats.map((stat) => (
<div key={stat.label} className="flex items-center justify-start text-xs gap-2">
<span className="font-semibold text-foreground">{stat.value}</span>
<span className="text-muted-foreground">{stat.label}</span>
</div>
))}
</div>
</div>
</CardContent>
</Card>
);
};

View file

@ -1,4 +1,5 @@
import type React from "react";
import { useFormatBytes } from "~/client/hooks/use-format-bytes";
type ByteSizeProps = {
bytes: number;
@ -12,72 +13,8 @@ type ByteSizeProps = {
fallback?: string; // shown if bytes is not a finite number (default: '—')
};
const SI_UNITS = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"] as const;
const IEC_UNITS = ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"] as const;
type FormatBytesResult = {
text: string;
unit: string;
unitIndex: number;
numeric: number; // numeric value before formatting (with sign)
};
export function formatBytes(
bytes: number,
options?: {
base?: 1000 | 1024;
maximumFractionDigits?: number;
smartRounding?: boolean;
locale?: string | string[];
},
): FormatBytesResult {
const { base = 1000, maximumFractionDigits = 2, smartRounding = true, locale } = options ?? {};
if (!Number.isFinite(bytes)) {
return {
text: "—",
unit: "",
unitIndex: 0,
numeric: NaN,
};
}
const units = base === 1024 ? IEC_UNITS : SI_UNITS;
const sign = Math.sign(bytes) || 1;
const abs = Math.abs(bytes);
let idx = 0;
if (abs > 0) {
idx = Math.floor(Math.log(abs) / Math.log(base));
if (!Number.isFinite(idx)) idx = 0;
idx = Math.max(0, Math.min(idx, units.length - 1));
}
const numeric = (abs / base ** idx) * sign;
const maxFrac = (() => {
if (!smartRounding) return maximumFractionDigits;
const v = Math.abs(numeric);
if (v >= 100) return 0;
if (v >= 10) return Math.min(1, maximumFractionDigits);
return maximumFractionDigits;
})();
const text = new Intl.NumberFormat(locale, {
minimumFractionDigits: 0,
maximumFractionDigits: maxFrac,
}).format(numeric);
return {
text,
unit: units[idx],
unitIndex: idx,
numeric,
};
}
export function ByteSize(props: ByteSizeProps) {
const formatBytes = useFormatBytes();
const {
bytes,
base = 1000,
@ -95,9 +32,10 @@ export function ByteSize(props: ByteSizeProps) {
maximumFractionDigits,
smartRounding,
locale,
fallback,
});
if (text === "—") {
if (text === fallback) {
return (
<span className={className} style={style}>
{fallback}

View file

@ -1,714 +0,0 @@
import { arktypeResolver } from "@hookform/resolvers/arktype";
import { type } from "arktype";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { cn, slugify } from "~/client/lib/utils";
import { deepClean } from "~/utils/object";
import { Button } from "./ui/button";
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "./ui/form";
import { Input } from "./ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
import { useQuery } from "@tanstack/react-query";
import { Alert, AlertDescription } from "./ui/alert";
import { ExternalLink, AlertTriangle } from "lucide-react";
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
import { useSystemInfo } from "~/client/hooks/use-system-info";
import { COMPRESSION_MODES, repositoryConfigSchema } from "~/schemas/restic";
import { listRcloneRemotesOptions } from "../api-client/@tanstack/react-query.gen";
import { Checkbox } from "./ui/checkbox";
import { DirectoryBrowser } from "./directory-browser";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "./ui/alert-dialog";
export const formSchema = type({
name: "2<=string<=32",
compressionMode: type.valueOf(COMPRESSION_MODES).optional(),
}).and(repositoryConfigSchema);
const cleanSchema = type.pipe((d) => formSchema(deepClean(d)));
export type RepositoryFormValues = typeof formSchema.inferIn;
type Props = {
onSubmit: (values: RepositoryFormValues) => void;
mode?: "create" | "update";
initialValues?: Partial<RepositoryFormValues>;
formId?: string;
loading?: boolean;
className?: string;
};
const defaultValuesForType = {
local: { backend: "local" as const, compressionMode: "auto" as const },
s3: { backend: "s3" as const, compressionMode: "auto" as const },
r2: { backend: "r2" as const, compressionMode: "auto" as const },
gcs: { backend: "gcs" as const, compressionMode: "auto" as const },
azure: { backend: "azure" as const, compressionMode: "auto" as const },
rclone: { backend: "rclone" as const, compressionMode: "auto" as const },
rest: { backend: "rest" as const, compressionMode: "auto" as const },
};
export const CreateRepositoryForm = ({
onSubmit,
mode = "create",
initialValues,
formId,
loading,
className,
}: Props) => {
const form = useForm<RepositoryFormValues>({
resolver: arktypeResolver(cleanSchema as unknown as typeof formSchema),
defaultValues: initialValues,
resetOptions: {
keepDefaultValues: true,
keepDirtyValues: false,
},
});
const { watch, setValue } = form;
const watchedBackend = watch("backend");
const watchedIsExistingRepository = watch("isExistingRepository");
const [passwordMode, setPasswordMode] = useState<"default" | "custom">("default");
const [showPathBrowser, setShowPathBrowser] = useState(false);
const [showPathWarning, setShowPathWarning] = useState(false);
const { capabilities } = useSystemInfo();
const { data: rcloneRemotes, isLoading: isLoadingRemotes } = useQuery({
...listRcloneRemotesOptions(),
enabled: capabilities.rclone,
});
useEffect(() => {
form.reset({
name: form.getValues().name,
isExistingRepository: form.getValues().isExistingRepository,
customPassword: form.getValues().customPassword,
...defaultValuesForType[watchedBackend as keyof typeof defaultValuesForType],
});
}, [watchedBackend, form]);
return (
<Form {...form}>
<form id={formId} onSubmit={form.handleSubmit(onSubmit)} className={cn("space-y-4", className)}>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input
{...field}
placeholder="Repository name"
onChange={(e) => field.onChange(slugify(e.target.value))}
max={32}
min={2}
disabled={mode === "update"}
className={mode === "update" ? "bg-gray-50" : ""}
/>
</FormControl>
<FormDescription>Unique identifier for the repository.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="backend"
render={({ field }) => (
<FormItem>
<FormLabel>Backend</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value} value={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a backend" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="local">Local</SelectItem>
<SelectItem value="s3">S3</SelectItem>
<SelectItem value="r2">Cloudflare R2</SelectItem>
<SelectItem value="gcs">Google Cloud Storage</SelectItem>
<SelectItem value="azure">Azure Blob Storage</SelectItem>
<SelectItem value="rest">REST Server</SelectItem>
<Tooltip>
<TooltipTrigger>
<SelectItem disabled={!capabilities.rclone} value="rclone">
rclone (40+ cloud providers)
</SelectItem>
</TooltipTrigger>
<TooltipContent className={cn({ hidden: capabilities.rclone })}>
<p>Setup rclone to use this backend</p>
</TooltipContent>
</Tooltip>
</SelectContent>
</Select>
<FormDescription>Choose the storage backend for this repository.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="compressionMode"
render={({ field }) => (
<FormItem>
<FormLabel>Compression Mode</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value} value={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select compression mode" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="off">Off</SelectItem>
<SelectItem value="auto">Auto</SelectItem>
<SelectItem value="fastest">Fastest</SelectItem>
<SelectItem value="better">Better</SelectItem>
<SelectItem value="max">Max</SelectItem>
</SelectContent>
</Select>
<FormDescription>Compression mode for backups stored in this repository.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="isExistingRepository"
render={({ field }) => (
<FormItem className="flex flex-row items-center space-x-3">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={(checked) => {
field.onChange(checked);
if (!checked) {
setPasswordMode("default");
setValue("customPassword", undefined);
}
}}
/>
</FormControl>
<div className="space-y-1">
<FormLabel>Import existing repository</FormLabel>
<FormDescription>Check this if the repository already exists at the specified location</FormDescription>
</div>
</FormItem>
)}
/>
{watchedIsExistingRepository && (
<>
<FormItem>
<FormLabel>Repository Password</FormLabel>
<Select
onValueChange={(value) => {
setPasswordMode(value as "default" | "custom");
if (value === "default") {
setValue("customPassword", undefined);
}
}}
defaultValue={passwordMode}
value={passwordMode}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select password option" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="default">Use Ironmount's password</SelectItem>
<SelectItem value="custom">Enter password manually</SelectItem>
</SelectContent>
</Select>
<FormDescription>
Choose whether to use Ironmount's master password or enter a custom password for the existing
repository.
</FormDescription>
</FormItem>
{passwordMode === "custom" && (
<FormField
control={form.control}
name="customPassword"
render={({ field }) => (
<FormItem>
<FormLabel>Repository Password</FormLabel>
<FormControl>
<Input type="password" placeholder="Enter repository password" {...field} />
</FormControl>
<FormDescription>
The password used to encrypt this repository. It will be stored securely.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
)}
</>
)}
{watchedBackend === "local" && (
<>
<FormItem>
<FormLabel>Repository Directory</FormLabel>
<div className="flex items-center gap-2">
<div className="flex-1 text-sm font-mono bg-muted px-3 py-2 rounded-md border">
{form.watch("path") || "/var/lib/ironmount/repositories"}
</div>
<Button
type="button"
variant="outline"
onClick={() => setShowPathWarning(true)}
size="sm"
>
Change
</Button>
</div>
<FormDescription>
The directory where the repository will be stored.
</FormDescription>
</FormItem>
<AlertDialog open={showPathWarning} onOpenChange={setShowPathWarning}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle className="flex items-center gap-2">
<AlertTriangle className="h-5 w-5 text-yellow-500" />
Important: Host Mount Required
</AlertDialogTitle>
<AlertDialogDescription className="space-y-3">
<p>
When selecting a custom path, ensure it is mounted from the host machine into the
container.
</p>
<p className="font-medium">
If the path is not a host mount, you will lose your repository data when the container
restarts.
</p>
<p className="text-sm text-muted-foreground">
The default path <code className="bg-muted px-1 rounded">/var/lib/ironmount/repositories</code> is
already mounted from the host and is safe to use.
</p>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={() => {
setShowPathBrowser(true);
setShowPathWarning(false);
}}
>
I Understand, Continue
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<AlertDialog open={showPathBrowser} onOpenChange={setShowPathBrowser}>
<AlertDialogContent className="max-w-2xl">
<AlertDialogHeader>
<AlertDialogTitle>Select Repository Directory</AlertDialogTitle>
<AlertDialogDescription>
Choose a directory from the filesystem to store the repository.
</AlertDialogDescription>
</AlertDialogHeader>
<div className="py-4">
<DirectoryBrowser
onSelectPath={(path) => form.setValue("path", path)}
selectedPath={form.watch("path") || "/var/lib/ironmount/repositories"}
/>
</div>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={() => setShowPathBrowser(false)}>Done</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
)}
{watchedBackend === "s3" && (
<>
<FormField
control={form.control}
name="endpoint"
render={({ field }) => (
<FormItem>
<FormLabel>Endpoint</FormLabel>
<FormControl>
<Input placeholder="s3.amazonaws.com" {...field} />
</FormControl>
<FormDescription>S3-compatible endpoint URL.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="bucket"
render={({ field }) => (
<FormItem>
<FormLabel>Bucket</FormLabel>
<FormControl>
<Input placeholder="my-backup-bucket" {...field} />
</FormControl>
<FormDescription>S3 bucket name for storing backups.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="accessKeyId"
render={({ field }) => (
<FormItem>
<FormLabel>Access Key ID</FormLabel>
<FormControl>
<Input placeholder="AKIAIOSFODNN7EXAMPLE" {...field} />
</FormControl>
<FormDescription>S3 access key ID for authentication.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="secretAccessKey"
render={({ field }) => (
<FormItem>
<FormLabel>Secret Access Key</FormLabel>
<FormControl>
<Input type="password" placeholder="••••••••" {...field} />
</FormControl>
<FormDescription>S3 secret access key for authentication.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</>
)}
{watchedBackend === "r2" && (
<>
<FormField
control={form.control}
name="endpoint"
render={({ field }) => (
<FormItem>
<FormLabel>Endpoint</FormLabel>
<FormControl>
<Input placeholder="<account-id>.r2.cloudflarestorage.com" {...field} />
</FormControl>
<FormDescription>
R2 endpoint (without https://). Find in R2 dashboard under bucket settings.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="bucket"
render={({ field }) => (
<FormItem>
<FormLabel>Bucket</FormLabel>
<FormControl>
<Input placeholder="my-backup-bucket" {...field} />
</FormControl>
<FormDescription>R2 bucket name for storing backups.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="accessKeyId"
render={({ field }) => (
<FormItem>
<FormLabel>Access Key ID</FormLabel>
<FormControl>
<Input placeholder="Access Key ID from R2 API tokens" {...field} />
</FormControl>
<FormDescription>R2 API token Access Key ID (create in Cloudflare R2 dashboard).</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="secretAccessKey"
render={({ field }) => (
<FormItem>
<FormLabel>Secret Access Key</FormLabel>
<FormControl>
<Input type="password" placeholder="••••••••" {...field} />
</FormControl>
<FormDescription>R2 API token Secret Access Key (shown once when creating token).</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</>
)}
{watchedBackend === "gcs" && (
<>
<FormField
control={form.control}
name="bucket"
render={({ field }) => (
<FormItem>
<FormLabel>Bucket</FormLabel>
<FormControl>
<Input placeholder="my-backup-bucket" {...field} />
</FormControl>
<FormDescription>GCS bucket name for storing backups.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="projectId"
render={({ field }) => (
<FormItem>
<FormLabel>Project ID</FormLabel>
<FormControl>
<Input placeholder="my-gcp-project-123" {...field} />
</FormControl>
<FormDescription>Google Cloud project ID.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="credentialsJson"
render={({ field }) => (
<FormItem>
<FormLabel>Service Account JSON</FormLabel>
<FormControl>
<Input type="password" placeholder="Paste service account JSON key..." {...field} />
</FormControl>
<FormDescription>Service account JSON credentials for authentication.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</>
)}
{watchedBackend === "azure" && (
<>
<FormField
control={form.control}
name="container"
render={({ field }) => (
<FormItem>
<FormLabel>Container</FormLabel>
<FormControl>
<Input placeholder="my-backup-container" {...field} />
</FormControl>
<FormDescription>Azure Blob Storage container name for storing backups.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="accountName"
render={({ field }) => (
<FormItem>
<FormLabel>Account Name</FormLabel>
<FormControl>
<Input placeholder="mystorageaccount" {...field} />
</FormControl>
<FormDescription>Azure Storage account name.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="accountKey"
render={({ field }) => (
<FormItem>
<FormLabel>Account Key</FormLabel>
<FormControl>
<Input type="password" placeholder="••••••••" {...field} />
</FormControl>
<FormDescription>Azure Storage account key for authentication.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="endpointSuffix"
render={({ field }) => (
<FormItem>
<FormLabel>Endpoint Suffix (Optional)</FormLabel>
<FormControl>
<Input placeholder="core.windows.net" {...field} />
</FormControl>
<FormDescription>Custom Azure endpoint suffix (defaults to core.windows.net).</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</>
)}
{watchedBackend === "rclone" &&
(!rcloneRemotes || rcloneRemotes.length === 0 ? (
<Alert>
<AlertDescription className="space-y-2">
<p className="font-medium">No rclone remotes configured</p>
<p className="text-sm text-muted-foreground">
To use rclone, you need to configure remotes on your host system
</p>
<a
href="https://rclone.org/docs/"
target="_blank"
rel="noopener noreferrer"
className="text-sm text-strong-accent inline-flex items-center gap-1"
>
View rclone documentation
<ExternalLink className="w-3 h-3" />
</a>
</AlertDescription>
</Alert>
) : (
<>
<FormField
control={form.control}
name="remote"
render={({ field }) => (
<FormItem>
<FormLabel>Remote</FormLabel>
<Select onValueChange={(v) => field.onChange(v)} defaultValue={field.value} value={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select an rclone remote" />
</SelectTrigger>
</FormControl>
<SelectContent>
{isLoadingRemotes ? (
<SelectItem value="loading" disabled>
Loading remotes...
</SelectItem>
) : (
rcloneRemotes.map((remote: { name: string; type: string }) => (
<SelectItem key={remote.name} value={remote.name}>
{remote.name} ({remote.type})
</SelectItem>
))
)}
</SelectContent>
</Select>
<FormDescription>Select the rclone remote configured on your host system.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="path"
render={({ field }) => (
<FormItem>
<FormLabel>Path</FormLabel>
<FormControl>
<Input placeholder="backups/ironmount" {...field} />
</FormControl>
<FormDescription>Path within the remote where backups will be stored.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</>
))}
{watchedBackend === "rest" && (
<>
<FormField
control={form.control}
name="url"
render={({ field }) => (
<FormItem>
<FormLabel>REST Server URL</FormLabel>
<FormControl>
<Input placeholder="http://192.168.1.30:8000" {...field} />
</FormControl>
<FormDescription>URL of the REST server.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="path"
render={({ field }) => (
<FormItem>
<FormLabel>Repository Path (Optional)</FormLabel>
<FormControl>
<Input placeholder="my-backup-repo" {...field} />
</FormControl>
<FormDescription>Path to the repository on the REST server (leave empty for root).</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username (Optional)</FormLabel>
<FormControl>
<Input placeholder="username" {...field} />
</FormControl>
<FormDescription>Username for REST server authentication.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password (Optional)</FormLabel>
<FormControl>
<Input type="password" placeholder="••••••••" {...field} />
</FormControl>
<FormDescription>Password for REST server authentication.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</>
)}
{mode === "update" && (
<Button type="submit" className="w-full" loading={loading}>
Save Changes
</Button>
)}
</form>
</Form>
);
};

View file

@ -1,595 +0,0 @@
import { arktypeResolver } from "@hookform/resolvers/arktype";
import { useMutation } from "@tanstack/react-query";
import { type } from "arktype";
import { CheckCircle, Loader2, XCircle } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { cn, slugify } from "~/client/lib/utils";
import { deepClean } from "~/utils/object";
import { DirectoryBrowser } from "./directory-browser";
import { Button } from "./ui/button";
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "./ui/form";
import { Input } from "./ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
import { volumeConfigSchema } from "~/schemas/volumes";
import { testConnectionMutation } from "../api-client/@tanstack/react-query.gen";
export const formSchema = type({
name: "2<=string<=32",
}).and(volumeConfigSchema);
const cleanSchema = type.pipe((d) => formSchema(deepClean(d)));
export type FormValues = typeof formSchema.inferIn;
type Props = {
onSubmit: (values: FormValues) => void;
mode?: "create" | "update";
initialValues?: Partial<FormValues>;
formId?: string;
loading?: boolean;
className?: string;
};
const defaultValuesForType = {
directory: { backend: "directory" as const, path: "/" },
nfs: { backend: "nfs" as const, port: 2049, version: "4.1" as const },
smb: { backend: "smb" as const, port: 445, vers: "3.0" as const },
webdav: { backend: "webdav" as const, port: 80, ssl: false },
};
export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, formId, loading, className }: Props) => {
const form = useForm<FormValues>({
resolver: arktypeResolver(cleanSchema as unknown as typeof formSchema),
defaultValues: initialValues,
resetOptions: {
keepDefaultValues: true,
keepDirtyValues: false,
},
});
const { watch, getValues } = form;
const watchedBackend = watch("backend");
useEffect(() => {
if (mode === "create") {
form.reset({
name: form.getValues().name,
...defaultValuesForType[watchedBackend as keyof typeof defaultValuesForType],
});
}
}, [watchedBackend, form, mode]);
const [testMessage, setTestMessage] = useState<{ success: boolean; message: string } | null>(null);
const testBackendConnection = useMutation({
...testConnectionMutation(),
onMutate: () => {
setTestMessage(null);
},
onError: (error) => {
setTestMessage({
success: false,
message: error?.message || "Failed to test connection. Please try again.",
});
},
onSuccess: (data) => {
setTestMessage(data);
},
});
const handleTestConnection = async () => {
const formValues = getValues();
if (formValues.backend === "nfs" || formValues.backend === "smb" || formValues.backend === "webdav") {
testBackendConnection.mutate({
body: { config: formValues },
});
}
};
return (
<Form {...form}>
<form id={formId} onSubmit={form.handleSubmit(onSubmit)} className={cn("space-y-4", className)}>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input
{...field}
placeholder="Volume name"
onChange={(e) => field.onChange(slugify(e.target.value))}
max={32}
min={1}
disabled={mode === "update"}
className={mode === "update" ? "bg-gray-50" : ""}
/>
</FormControl>
<FormDescription>Unique identifier for the volume.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="backend"
render={({ field }) => (
<FormItem>
<FormLabel>Backend</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value} value={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a backend" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="directory">Directory</SelectItem>
<SelectItem value="nfs">NFS</SelectItem>
<SelectItem value="smb">SMB</SelectItem>
<SelectItem value="webdav">WebDAV</SelectItem>
</SelectContent>
</Select>
<FormDescription>Choose the storage backend for this volume.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
{watchedBackend === "directory" && (
<FormField
control={form.control}
name="path"
render={({ field }) => {
return (
<FormItem>
<FormLabel>Directory Path</FormLabel>
<FormControl>
{field.value ? (
<div className="flex items-center gap-2">
<div className="flex-1 border rounded-md p-3 bg-muted/50">
<div className="text-xs font-medium text-muted-foreground mb-1">Selected path:</div>
<div className="text-sm font-mono break-all">{field.value}</div>
</div>
<Button type="button" variant="outline" size="sm" onClick={() => field.onChange("")}>
Change
</Button>
</div>
) : (
<DirectoryBrowser onSelectPath={(path) => field.onChange(path)} selectedPath={field.value} />
)}
</FormControl>
<FormDescription>Browse and select a directory on the host filesystem to track.</FormDescription>
<FormMessage />
</FormItem>
);
}}
/>
)}
{watchedBackend === "nfs" && (
<>
<FormField
control={form.control}
name="server"
render={({ field }) => (
<FormItem>
<FormLabel>Server</FormLabel>
<FormControl>
<Input placeholder="192.168.1.100" {...field} />
</FormControl>
<FormDescription>NFS server IP address or hostname.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="exportPath"
render={({ field }) => (
<FormItem>
<FormLabel>Export Path</FormLabel>
<FormControl>
<Input placeholder="/export/data" {...field} />
</FormControl>
<FormDescription>Path to the NFS export on the server.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="port"
defaultValue={2049}
render={({ field }) => (
<FormItem>
<FormLabel>Port</FormLabel>
<FormControl>
<Input
type="number"
placeholder="2049"
{...field}
onChange={(e) => field.onChange(parseInt(e.target.value, 10) || undefined)}
/>
</FormControl>
<FormDescription>NFS server port (default: 2049).</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="version"
defaultValue="4.1"
render={({ field }) => (
<FormItem>
<FormLabel>Version</FormLabel>
<Select onValueChange={field.onChange} defaultValue="4.1">
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select NFS version" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="3">NFS v3</SelectItem>
<SelectItem value="4">NFS v4</SelectItem>
<SelectItem value="4.1">NFS v4.1</SelectItem>
</SelectContent>
</Select>
<FormDescription>NFS protocol version to use.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="readOnly"
defaultValue={false}
render={({ field }) => (
<FormItem>
<FormLabel>Read-only Mode</FormLabel>
<FormControl>
<div className="flex items-center space-x-2">
<input
type="checkbox"
checked={field.value ?? false}
onChange={(e) => field.onChange(e.target.checked)}
className="rounded border-gray-300"
/>
<span className="text-sm">Mount volume as read-only</span>
</div>
</FormControl>
<FormDescription>
Prevent any modifications to the volume. Recommended for backup sources and sensitive data.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</>
)}
{watchedBackend === "webdav" && (
<>
<FormField
control={form.control}
name="server"
render={({ field }) => (
<FormItem>
<FormLabel>Server</FormLabel>
<FormControl>
<Input placeholder="example.com" {...field} />
</FormControl>
<FormDescription>WebDAV server hostname or IP address.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="path"
render={({ field }) => (
<FormItem>
<FormLabel>Path</FormLabel>
<FormControl>
<Input placeholder="/webdav" {...field} />
</FormControl>
<FormDescription>Path to the WebDAV directory on the server.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username (Optional)</FormLabel>
<FormControl>
<Input placeholder="admin" {...field} />
</FormControl>
<FormDescription>Username for WebDAV authentication (optional).</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password (Optional)</FormLabel>
<FormControl>
<Input type="password" placeholder="••••••••" {...field} />
</FormControl>
<FormDescription>Password for WebDAV authentication (optional).</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="port"
defaultValue={80}
render={({ field }) => (
<FormItem>
<FormLabel>Port</FormLabel>
<FormControl>
<Input
type="number"
placeholder="80"
{...field}
onChange={(e) => field.onChange(parseInt(e.target.value, 10) || undefined)}
/>
</FormControl>
<FormDescription>WebDAV server port (default: 80 for HTTP, 443 for HTTPS).</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="ssl"
defaultValue={false}
render={({ field }) => (
<FormItem>
<FormLabel>Use SSL/HTTPS</FormLabel>
<FormControl>
<div className="flex items-center space-x-2">
<input
type="checkbox"
checked={field.value ?? false}
onChange={(e) => field.onChange(e.target.checked)}
className="rounded border-gray-300"
/>
<span className="text-sm">Enable HTTPS for secure connections</span>
</div>
</FormControl>
<FormDescription>Use HTTPS instead of HTTP for secure connections.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="readOnly"
defaultValue={false}
render={({ field }) => (
<FormItem>
<FormLabel>Read-only Mode</FormLabel>
<FormControl>
<div className="flex items-center space-x-2">
<input
type="checkbox"
checked={field.value ?? false}
onChange={(e) => field.onChange(e.target.checked)}
className="rounded border-gray-300"
/>
<span className="text-sm">Mount volume as read-only</span>
</div>
</FormControl>
<FormDescription>
Prevent any modifications to the volume. Recommended for backup sources and sensitive data.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</>
)}
{watchedBackend === "smb" && (
<>
<FormField
control={form.control}
name="server"
render={({ field }) => (
<FormItem>
<FormLabel>Server</FormLabel>
<FormControl>
<Input placeholder="192.168.1.100" value={field.value} onChange={field.onChange} />
</FormControl>
<FormDescription>SMB server IP address or hostname.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="share"
render={({ field }) => (
<FormItem>
<FormLabel>Share</FormLabel>
<FormControl>
<Input placeholder="myshare" value={field.value} onChange={field.onChange} />
</FormControl>
<FormDescription>SMB share name on the server.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="admin" value={field.value} onChange={field.onChange} />
</FormControl>
<FormDescription>Username for SMB authentication.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input type="password" placeholder="••••••••" value={field.value} onChange={field.onChange} />
</FormControl>
<FormDescription>Password for SMB authentication.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="vers"
defaultValue="3.0"
render={({ field }) => (
<FormItem>
<FormLabel>SMB Version</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value || "3.0"}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select SMB version" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="1.0">SMB v1.0</SelectItem>
<SelectItem value="2.0">SMB v2.0</SelectItem>
<SelectItem value="2.1">SMB v2.1</SelectItem>
<SelectItem value="3.0">SMB v3.0</SelectItem>
</SelectContent>
</Select>
<FormDescription>SMB protocol version to use (default: 3.0).</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="domain"
render={({ field }) => (
<FormItem>
<FormLabel>Domain (Optional)</FormLabel>
<FormControl>
<Input placeholder="WORKGROUP" value={field.value} onChange={field.onChange} />
</FormControl>
<FormDescription>Domain or workgroup for authentication (optional).</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="port"
defaultValue={445}
render={({ field }) => (
<FormItem>
<FormLabel>Port</FormLabel>
<FormControl>
<Input
type="number"
placeholder="445"
value={field.value}
defaultValue={445}
onChange={(e) => field.onChange(parseInt(e.target.value, 10) || undefined)}
/>
</FormControl>
<FormDescription>SMB server port (default: 445).</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="readOnly"
defaultValue={false}
render={({ field }) => (
<FormItem>
<FormLabel>Read-only Mode</FormLabel>
<FormControl>
<div className="flex items-center space-x-2">
<input
type="checkbox"
checked={field.value ?? false}
onChange={(e) => field.onChange(e.target.checked)}
className="rounded border-gray-300"
/>
<span className="text-sm">Mount volume as read-only</span>
</div>
</FormControl>
<FormDescription>
Prevent any modifications to the volume. Recommended for backup sources and sensitive data.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</>
)}
{watchedBackend !== "directory" && (
<div className="space-y-3">
<div className="flex items-center gap-2">
<Button
type="button"
variant="outline"
onClick={handleTestConnection}
disabled={testBackendConnection.isPending}
className="flex-1"
>
{testBackendConnection.isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{!testBackendConnection.isPending && testMessage?.success && (
<CheckCircle className="mr-2 h-4 w-4 text-green-500" />
)}
{!testBackendConnection.isPending && testMessage && !testMessage.success && (
<XCircle className="mr-2 h-4 w-4 text-red-500" />
)}
{testBackendConnection.isPending
? "Testing..."
: testMessage
? testMessage.success
? "Connection Successful"
: "Test Failed"
: "Test Connection"}
</Button>
</div>
{testMessage && (
<div
className={cn("text-xs p-2 rounded-md text-wrap wrap-anywhere", {
"bg-green-50 text-green-700 border border-green-200": testMessage.success,
"bg-red-50 text-red-700 border border-red-200": !testMessage.success,
})}
>
{testMessage.message}
</div>
)}
</div>
)}
{mode === "update" && (
<Button type="submit" className="w-full" loading={loading}>
Save Changes
</Button>
)}
</form>
</Form>
);
};

View file

@ -0,0 +1,92 @@
import { CronExpressionParser } from "cron-parser";
import { AlertCircle, CheckCircle2 } from "lucide-react";
import { useMemo } from "react";
import { FormControl, FormDescription, FormItem, FormLabel, FormMessage } from "~/client/components/ui/form";
import { Input } from "~/client/components/ui/input";
import { useTimeFormat } from "~/client/lib/datetime";
import { cn } from "~/client/lib/utils";
interface CronInputProps {
value: string;
onChange: (value: string) => void;
error?: string;
}
export function CronInput({ value, onChange, error }: CronInputProps) {
const { formatDateTime } = useTimeFormat();
const { isValid, nextRuns, parseError } = useMemo(() => {
if (!value) {
return { isValid: false, nextRuns: [], parseError: null };
}
const parts = value.trim().split(/\s+/);
if (parts.length !== 5) {
return {
isValid: false,
nextRuns: [],
parseError: "Expression must have exactly 5 fields (minute, hour, day, month, day-of-week)",
};
}
try {
const interval = CronExpressionParser.parse(value);
const runs: Date[] = [];
for (let i = 0; i < 5; i++) {
runs.push(interval.next().toDate());
}
return { isValid: true, nextRuns: runs, parseError: null };
} catch (e) {
return { isValid: false, nextRuns: [], parseError: (e as Error).message };
}
}, [value]);
return (
<FormItem className="md:col-span-2">
<FormLabel>Cron expression</FormLabel>
<FormControl>
<div className="relative">
<Input
placeholder="* * * * *"
value={value}
onChange={(e) => onChange(e.target.value)}
className={cn("font-mono", {
"border-destructive": error || (value && !isValid),
})}
/>
<div className="absolute right-3 top-1/2 -translate-y-1/2">
{value && (
<div>
{isValid ? (
<CheckCircle2 className="h-4 w-4 text-success" />
) : (
<AlertCircle className="h-4 w-4 text-destructive" />
)}
</div>
)}
</div>
</div>
</FormControl>
<FormDescription>
Standard cron format: <code className="bg-muted px-1 rounded">minute hour day month day-of-week</code>.
</FormDescription>
{value && !isValid && parseError && <p className="text-xs text-destructive mt-1">{parseError}</p>}
{isValid && nextRuns.length > 0 && (
<div className="mt-2 p-3 rounded-md bg-muted/50 border border-border">
<p className="text-xs font-medium mb-2 text-muted-foreground uppercase tracking-wider">Next 5 executions:</p>
<ul className="space-y-1">
{nextRuns.map((date, i) => (
<li key={date.toISOString()} className="text-xs font-mono flex items-center gap-2">
<span className="text-muted-foreground w-4">{i + 1}.</span>
{formatDateTime(date)}
</li>
))}
</ul>
</div>
)}
<FormMessage />
</FormItem>
);
}

View file

@ -0,0 +1,54 @@
import type { Column } from "@tanstack/react-table";
import { ArrowDown, ArrowUp, ArrowUpDown } from "lucide-react";
import { Button } from "~/client/components/ui/button";
import { cn } from "~/client/lib/utils";
export function DataTableSortHeader<TData, TValue>({
column,
title,
sortDirection,
center = false,
}: {
column: Column<TData, TValue>;
title: string;
sortDirection: false | "asc" | "desc";
center?: boolean;
}) {
const icon =
sortDirection === "desc" ? (
<ArrowDown className="ml-2 h-3.5 w-3.5" />
) : sortDirection === "asc" ? (
<ArrowUp className="ml-2 h-3.5 w-3.5" />
) : (
<ArrowUpDown className="ml-2 h-3.5 w-3.5" />
);
const iconVisibility = sortDirection ? "" : "lg:invisible lg:group-hover/sort:visible";
if (center) {
return (
<Button
type="button"
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
className="h-auto! w-full! p-0! font-inherit hover:bg-transparent uppercase group/sort relative"
>
<span className="relative flex w-full items-center justify-center">
{title}
<span className={cn("lg:absolute lg:-right-6 lg:top-1/2 lg:-translate-y-1/2", iconVisibility)}>{icon}</span>
</span>
</Button>
);
}
return (
<Button
type="button"
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
className="h-auto! p-0! font-inherit hover:bg-transparent uppercase group/sort"
>
{title}
<span className={iconVisibility}>{icon}</span>
</Button>
);
}

View file

@ -0,0 +1,25 @@
import { useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { useHotkeySequence } from "@tanstack/react-hotkeys";
import { getDevPanelOptions } from "~/client/api-client/@tanstack/react-query.gen";
import { DevPanel } from "./dev-panel";
export function DevPanelListener() {
const [isOpen, setIsOpen] = useState(false);
const { data: devPanelStatus } = useQuery({
...getDevPanelOptions(),
});
useHotkeySequence(["D", "E", "V"], () => setIsOpen(true), {
enabled: !!devPanelStatus?.enabled,
preventDefault: true,
ignoreInputs: true,
timeout: 1000,
});
if (!devPanelStatus?.enabled) {
return null;
}
return <DevPanel open={isOpen} onOpenChange={setIsOpen} />;
}

View file

@ -0,0 +1,229 @@
import { useRef, useState, useCallback } from "react";
import { useQuery } from "@tanstack/react-query";
import { Play, Loader2, Trash2, Terminal } from "lucide-react";
import { Button } from "./ui/button";
import { cn } from "~/client/lib/utils";
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription } from "./ui/sheet";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
import { Label } from "./ui/label";
import { Textarea } from "./ui/textarea";
import { listRepositoriesOptions } from "~/client/api-client/@tanstack/react-query.gen";
import { devPanelExec } from "~/client/api-client/sdk.gen";
import { parseError } from "../lib/errors";
type DevPanelProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
};
type SseOutputEvent = {
type: "stdout" | "stderr";
line: string;
};
type SseDoneEvent = {
type: "done";
exitCode: number;
};
type SseErrorEvent = {
type: "error";
message: string;
};
type SseEvent = SseOutputEvent | SseDoneEvent | SseErrorEvent;
export function DevPanel({ open, onOpenChange }: DevPanelProps) {
const { data: repositories = [] } = useQuery({
...listRepositoriesOptions(),
enabled: open,
});
const [selectedRepoId, setSelectedRepoId] = useState<string>("");
const [commandLine, setCommandLine] = useState("snapshots");
const [output, setOutput] = useState<string[]>([]);
const [isRunning, setIsRunning] = useState(false);
const abortControllerRef = useRef<AbortController | null>(null);
const outputRef = useRef<HTMLDivElement>(null);
const scrollToBottom = useCallback(() => {
if (outputRef.current) {
outputRef.current.scrollTop = outputRef.current.scrollHeight;
}
}, []);
const appendOutput = useCallback(
(line: string) => {
setOutput((prev) => {
const newOutput = [...prev, line];
setTimeout(scrollToBottom, 0);
return newOutput;
});
},
[scrollToBottom],
);
const handleRun = async () => {
if (!selectedRepoId || !commandLine.trim()) {
return;
}
setOutput([]);
setIsRunning(true);
appendOutput(`$ restic ${commandLine}`.trim());
appendOutput("---");
abortControllerRef.current = new AbortController();
const trimmedLine = commandLine.trim();
const parts = trimmedLine.split(/\s+/);
const command = parts[0];
const argsArray = parts.slice(1);
try {
const result = await devPanelExec({
path: { shortId: selectedRepoId },
body: { command, args: argsArray.length > 0 ? argsArray : undefined },
signal: abortControllerRef.current.signal,
});
for await (const event of result.stream) {
if (abortControllerRef.current.signal.aborted) {
break;
}
const data = event as unknown as SseEvent;
if (!data || typeof data !== "object") {
continue;
}
if (data.type === "stdout" || data.type === "stderr") {
appendOutput(data.line);
} else if (data.type === "done") {
appendOutput(`---`);
appendOutput(`Command finished with exit code: ${data.exitCode}`);
} else if (data.type === "error") {
appendOutput(`Error: ${data.message}`);
}
}
} catch (err) {
if (err instanceof Error && err.name === "AbortError") {
appendOutput("---");
appendOutput("Command cancelled");
} else {
appendOutput(`Error: ${parseError(err)?.message}`);
}
} finally {
setIsRunning(false);
abortControllerRef.current = null;
}
};
const handleCancel = () => {
abortControllerRef.current?.abort();
};
const handleClear = () => {
setOutput([]);
};
const handleClose = () => {
if (isRunning) {
handleCancel();
}
onOpenChange(false);
};
return (
<Sheet open={open} onOpenChange={handleClose}>
<SheetContent side="right" className="w-full sm:max-w-xl flex flex-col px-4">
<SheetHeader>
<SheetTitle className="flex items-center gap-2">
<Terminal className="h-5 w-5" />
Dev Panel
</SheetTitle>
<SheetDescription>Execute restic commands against a repository</SheetDescription>
</SheetHeader>
<div className="flex flex-col gap-4 flex-1 min-h-0 mt-4">
<div className="grid gap-4">
<div className="grid gap-2">
<Label htmlFor="repository">Repository</Label>
<Select value={selectedRepoId} onValueChange={setSelectedRepoId}>
<SelectTrigger id="repository">
<SelectValue placeholder="Select a repository" />
</SelectTrigger>
<SelectContent>
{repositories.map((repo) => (
<SelectItem key={repo.id} value={repo.shortId}>
{repo.name} ({repo.type})
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="grid gap-2">
<Label htmlFor="command">Command</Label>
<Textarea
id="command"
placeholder="e.g., snapshots, check --dry-run, forget --keep-last 10"
value={commandLine}
onChange={(e) => setCommandLine(e.target.value)}
className="font-mono text-sm"
rows={2}
/>
</div>
<div className="flex gap-2">
<Button onClick={handleRun} disabled={isRunning || !selectedRepoId || !commandLine.trim()}>
<Loader2 className={cn("h-4 w-4 animate-spin mr-2", { hidden: !isRunning })} />
<Play className={cn("h-4 w-4 mr-2", { hidden: isRunning })} />
<span className={cn({ hidden: !isRunning })}>Running...</span>
<span className={cn({ hidden: isRunning })}>Run</span>
</Button>
<Button variant="destructive" onClick={handleCancel} className={cn({ hidden: !isRunning })}>
Cancel
</Button>
<Button variant="outline" onClick={handleClear} disabled={isRunning}>
<Trash2 className="h-4 w-4 mr-2" />
Clear
</Button>
</div>
</div>
<div className="flex-1 min-h-0 border rounded-md bg-muted/30">
<div ref={outputRef} className="h-full overflow-auto p-3 font-mono text-xs">
<div className={cn("text-muted-foreground", { hidden: output.length > 0 })}>
Output will appear here...
</div>
<div className={cn("space-y-0.5", { hidden: output.length === 0 })}>
{output.map((line, i) => {
let displayLine = line;
let isJson = false;
try {
const parsed = JSON.parse(line);
displayLine = JSON.stringify(parsed, null, 2);
isJson = true;
} catch {
// Not valid JSON, display as-is
}
return (
<pre
key={`${i}-${line.slice(0, 20)}`}
className={cn("whitespace-pre-wrap break-all text-xs", {
"wrap-break-word text-[10px] leading-tight": isJson,
})}
>
{displayLine}
</pre>
);
})}
</div>
</div>
</div>
</div>
</SheetContent>
</Sheet>
);
}

View file

@ -1,74 +0,0 @@
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { FileTree } from "./file-tree";
import { ScrollArea } from "./ui/scroll-area";
import { browseFilesystemOptions } from "../api-client/@tanstack/react-query.gen";
import { useFileBrowser } from "../hooks/use-file-browser";
type Props = {
onSelectPath: (path: string) => void;
selectedPath?: string;
};
export const DirectoryBrowser = ({ onSelectPath, selectedPath }: Props) => {
const queryClient = useQueryClient();
const { data, isLoading } = useQuery({
...browseFilesystemOptions({ query: { path: "/" } }),
});
const fileBrowser = useFileBrowser({
initialData: data,
isLoading,
fetchFolder: async (path) => {
return await queryClient.ensureQueryData(browseFilesystemOptions({ query: { path } }));
},
prefetchFolder: (path) => {
queryClient.prefetchQuery(browseFilesystemOptions({ query: { path } }));
},
});
if (fileBrowser.isLoading) {
return (
<div className="border rounded-lg overflow-hidden">
<ScrollArea className="h-64">
<div className="text-sm text-gray-500 p-4">Loading directories...</div>
</ScrollArea>
</div>
);
}
if (fileBrowser.isEmpty) {
return (
<div className="border rounded-lg overflow-hidden">
<ScrollArea className="h-64">
<div className="text-sm text-gray-500 p-4">No subdirectories found</div>
</ScrollArea>
</div>
);
}
return (
<div className="border rounded-lg overflow-hidden">
<ScrollArea className="h-64">
<FileTree
files={fileBrowser.fileArray}
onFolderExpand={fileBrowser.handleFolderExpand}
onFolderHover={fileBrowser.handleFolderHover}
expandedFolders={fileBrowser.expandedFolders}
loadingFolders={fileBrowser.loadingFolders}
foldersOnly
selectableFolders
selectedFolder={selectedPath}
onFolderSelect={onSelectPath}
/>
</ScrollArea>
{selectedPath && (
<div className="bg-muted/50 border-t p-2 text-sm">
<div className="font-medium text-muted-foreground">Selected path:</div>
<div className="font-mono text-xs break-all">{selectedPath}</div>
</div>
)}
</div>
);
};

View file

@ -0,0 +1,244 @@
import type { ComponentProps } from "react";
import { afterEach, describe, expect, test } from "vitest";
import { HttpResponse, http, server } from "~/test/msw/server";
import { cleanup, fireEvent, render, screen, userEvent, waitFor, within } from "~/test/test-utils";
type SnapshotFilesRequest = {
shortId: string;
snapshotId: string;
path: string | null;
offset: string | null;
limit: string | null;
};
const snapshotFiles = {
files: [
{ name: "project", path: "/mnt/project", type: "dir" },
{ name: "a.txt", path: "/mnt/project/a.txt", type: "file" },
],
};
import { SnapshotTreeBrowser } from "../snapshot-tree-browser";
const mockListSnapshotFiles = (response = snapshotFiles) => {
const requests: SnapshotFilesRequest[] = [];
server.use(
http.get("/api/v1/repositories/:shortId/snapshots/:snapshotId/files", ({ params, request }) => {
const url = new URL(request.url);
requests.push({
shortId: String(params.shortId),
snapshotId: String(params.snapshotId),
path: url.searchParams.get("path"),
offset: url.searchParams.get("offset"),
limit: url.searchParams.get("limit"),
});
return HttpResponse.json(response);
}),
);
return requests;
};
const renderSnapshotTreeBrowser = (props: Partial<ComponentProps<typeof SnapshotTreeBrowser>> = {}) => {
return render(
<SnapshotTreeBrowser
repositoryId="repo-1"
snapshotId="snap-1"
queryBasePath="/mnt/project"
displayBasePath="/mnt"
{...props}
/>,
);
};
afterEach(() => {
cleanup();
});
describe("SnapshotTreeBrowser", () => {
test("renders the query root folder when display base path is broader than query base path", async () => {
mockListSnapshotFiles();
renderSnapshotTreeBrowser();
expect(await screen.findByRole("button", { name: "project" })).toBeTruthy();
});
test("renders ancestor folders when the query root is nested multiple levels below the display root", async () => {
mockListSnapshotFiles({
files: [
{ name: "subdir", path: "/mnt/project/subdir", type: "dir" },
{ name: "a.txt", path: "/mnt/project/subdir/a.txt", type: "file" },
],
});
renderSnapshotTreeBrowser({
queryBasePath: "/mnt/project/subdir",
displayBasePath: "/mnt",
});
expect(await screen.findByRole("button", { name: "project" })).toBeTruthy();
});
test("renders synthesized ancestor folders for a single file when no display base path is available", async () => {
const requests = mockListSnapshotFiles({
files: [{ name: "report.txt", path: "/mnt/project/report.txt", type: "file" }],
});
renderSnapshotTreeBrowser({
queryBasePath: "/mnt/project/report.txt",
displayBasePath: undefined,
});
const mntRow = await screen.findByRole("button", { name: "mnt" });
const mntExpandIcon = mntRow.querySelector("svg");
if (!mntExpandIcon) {
throw new Error("Expected expand icon for mnt row");
}
fireEvent.click(mntExpandIcon);
expect(await screen.findByRole("button", { name: "project" })).toBeTruthy();
expect(requests[0]).toEqual({
shortId: "repo-1",
snapshotId: "snap-1",
path: "/mnt/project/report.txt",
offset: null,
limit: null,
});
});
test("returns the ancestor folder path when selecting above the query root", async () => {
mockListSnapshotFiles({
files: [
{ name: "subdir", path: "/mnt/project/subdir", type: "dir" },
{ name: "a.txt", path: "/mnt/project/subdir/a.txt", type: "file" },
],
});
let selectedPaths: Set<string> | undefined;
let selectedKind: "file" | "dir" | null = null;
renderSnapshotTreeBrowser({
queryBasePath: "/mnt/project/subdir",
displayBasePath: "/mnt",
withCheckboxes: true,
onSelectionChange: (paths) => {
selectedPaths = paths;
},
onSingleSelectionKindChange: (kind) => {
selectedKind = kind;
},
});
const row = await screen.findByRole("button", { name: "project" });
const checkbox = within(row).getByRole("checkbox");
await userEvent.click(checkbox);
expect(selectedPaths ? Array.from(selectedPaths) : []).toEqual(["/mnt/project"]);
expect(selectedKind === "dir").toBe(true);
});
test("keeps full paths when the display root does not contain the query root", async () => {
mockListSnapshotFiles();
let selectedPaths: Set<string> | undefined;
let selectedKind: "file" | "dir" | null = null;
renderSnapshotTreeBrowser({
queryBasePath: "/mnt/project",
displayBasePath: "/other/root",
withCheckboxes: true,
onSelectionChange: (paths) => {
selectedPaths = paths;
},
onSingleSelectionKindChange: (kind) => {
selectedKind = kind;
},
});
const row = await screen.findByRole("button", { name: "mnt" });
const checkbox = within(row).getByRole("checkbox");
await userEvent.click(checkbox);
expect(selectedPaths ? Array.from(selectedPaths) : []).toEqual(["/mnt"]);
expect(selectedKind === "dir").toBe(true);
});
test("shows selected folder state when full paths are provided from the parent", async () => {
mockListSnapshotFiles();
renderSnapshotTreeBrowser({
withCheckboxes: true,
selectedPaths: new Set(["/mnt/project"]),
onSelectionChange: () => {},
});
const row = await screen.findByRole("button", { name: "project" });
const checkbox = within(row).getByRole("checkbox");
expect(checkbox.getAttribute("aria-checked")).toBe("true");
});
test("returns the full snapshot path and kind when selecting a displayed folder", async () => {
mockListSnapshotFiles();
let selectedPaths: Set<string> | undefined;
let selectedKind: "file" | "dir" | null = null;
renderSnapshotTreeBrowser({
withCheckboxes: true,
onSelectionChange: (paths) => {
selectedPaths = paths;
},
onSingleSelectionKindChange: (kind) => {
selectedKind = kind;
},
});
const row = await screen.findByRole("button", { name: "project" });
const checkbox = within(row).getByRole("checkbox");
await userEvent.click(checkbox);
expect(selectedPaths ? Array.from(selectedPaths) : []).toEqual(["/mnt/project"]);
expect(selectedKind === "dir").toBe(true);
});
test("uses the query base path for the initial request when display base path is broader", async () => {
const requests = mockListSnapshotFiles();
renderSnapshotTreeBrowser();
await waitFor(() => {
expect(requests[0]).toEqual({
shortId: "repo-1",
snapshotId: "snap-1",
path: "/mnt/project",
offset: null,
limit: null,
});
});
});
test("shows the query root contents when display and query roots differ", async () => {
mockListSnapshotFiles();
renderSnapshotTreeBrowser();
const row = await screen.findByRole("button", { name: "project" });
const expandIcon = row.querySelector("svg");
if (!expandIcon) {
throw new Error("Expected expand icon for folder row");
}
if (!screen.queryByRole("button", { name: "a.txt" })) {
fireEvent.click(expandIcon);
}
expect(await screen.findByRole("button", { name: "a.txt" })).toBeTruthy();
});
});

View file

@ -0,0 +1,24 @@
import { LocalFileBrowser } from "./local-file-browser";
type Props = {
onSelectPath: (path: string) => void;
selectedPath?: string;
};
export const DirectoryBrowser = ({ onSelectPath, selectedPath }: Props) => {
return (
<LocalFileBrowser
className="border rounded-lg overflow-hidden"
useScrollArea
scrollAreaClassName="h-64"
foldersOnly
selectableFolders
selectedFolder={selectedPath}
onFolderSelect={onSelectPath}
showSelectedPathFooter
selectedPath={selectedPath}
loadingMessage="Loading directories..."
emptyMessage="No subdirectories found"
/>
);
};

View file

@ -0,0 +1,149 @@
import { type ReactNode } from "react";
import { FolderOpen } from "lucide-react";
import { FileTree, type FileEntry } from "~/client/components/file-tree";
import { ScrollArea } from "~/client/components/ui/scroll-area";
import { cn } from "~/client/lib/utils";
type PaginationState = {
hasMore: boolean;
isLoadingMore: boolean;
};
export type FileBrowserUiProps = {
className?: string;
treeContainerClassName?: string;
treeClassName?: string;
useScrollArea?: boolean;
scrollAreaClassName?: string;
stateClassName?: string;
loadingMessage?: string;
emptyMessage?: string;
emptyDescription?: string;
emptyIcon?: ReactNode;
withCheckboxes?: boolean;
selectedPaths?: Set<string>;
onSelectionChange?: (paths: Set<string>) => void;
foldersOnly?: boolean;
selectableFolders?: boolean;
onFolderSelect?: (folderPath: string) => void;
selectedFolder?: string;
onFileSelect?: (filePath: string) => void;
selectedFile?: string;
showSelectedPathFooter?: boolean;
selectedPath?: string;
selectedPathLabel?: string;
};
type FileBrowserProps = FileBrowserUiProps & {
isLoading: boolean;
isEmpty: boolean;
errorMessage?: string;
fileArray: FileEntry[];
expandedFolders: Set<string>;
loadingFolders: Set<string>;
onFolderExpand: (folderPath: string) => void | Promise<void>;
onFolderHover: (folderPath: string) => void;
onLoadMore: (folderPath: string) => void | Promise<void>;
getFolderPagination: (folderPath: string) => PaginationState;
};
export const FileBrowser = (props: FileBrowserProps) => {
const {
className,
treeContainerClassName,
treeClassName,
useScrollArea = false,
scrollAreaClassName,
stateClassName,
loadingMessage = "Loading files...",
emptyMessage = "No files found.",
emptyDescription,
emptyIcon,
withCheckboxes = false,
selectedPaths,
onSelectionChange,
foldersOnly = false,
selectableFolders = false,
onFolderSelect,
selectedFolder,
onFileSelect,
selectedFile,
showSelectedPathFooter = false,
selectedPath,
selectedPathLabel = "Selected path:",
isLoading,
isEmpty,
errorMessage,
fileArray,
expandedFolders,
loadingFolders,
onFolderExpand,
onFolderHover,
onLoadMore,
getFolderPagination,
} = props;
const resolvedSelectedPath = selectedPath ?? selectedFolder;
const resolvedEmptyIcon =
emptyIcon === undefined ? <FolderOpen className="mb-2 h-12 w-12 text-muted-foreground" /> : emptyIcon;
let body: ReactNode;
if (isLoading) {
body = (
<div className={cn("flex min-h-50 flex-col items-center justify-center p-6 text-center", stateClassName)}>
<p className="text-muted-foreground">{loadingMessage}</p>
</div>
);
} else if (errorMessage) {
body = (
<div className={cn("flex min-h-50 flex-col items-center justify-center p-6 text-center", stateClassName)}>
<p className="text-destructive">{errorMessage}</p>
</div>
);
} else if (isEmpty) {
body = (
<div className={cn("flex min-h-50 flex-col items-center justify-center p-6 text-center", stateClassName)}>
{resolvedEmptyIcon}
<p className="text-muted-foreground">{emptyMessage}</p>
{emptyDescription && <p className="mt-2 text-sm text-muted-foreground">{emptyDescription}</p>}
</div>
);
} else {
body = (
<FileTree
files={fileArray}
onFolderExpand={onFolderExpand}
onFolderHover={onFolderHover}
onLoadMore={onLoadMore}
getFolderPagination={getFolderPagination}
expandedFolders={expandedFolders}
loadingFolders={loadingFolders}
className={treeClassName}
withCheckboxes={withCheckboxes}
selectedPaths={selectedPaths}
onSelectionChange={onSelectionChange}
foldersOnly={foldersOnly}
selectableFolders={selectableFolders}
onFolderSelect={onFolderSelect}
selectedFolder={selectedFolder}
onFileSelect={onFileSelect}
selectedFile={selectedFile}
/>
);
}
const bodyWithScroll = useScrollArea ? <ScrollArea className={scrollAreaClassName}>{body}</ScrollArea> : body;
return (
<div className={className}>
<div className={treeContainerClassName}>{bodyWithScroll}</div>
{showSelectedPathFooter && resolvedSelectedPath && (
<div className="bg-muted/50 border-t p-2 text-sm">
<div className="font-medium text-muted-foreground">{selectedPathLabel}</div>
<div className="font-mono text-xs break-all">{resolvedSelectedPath}</div>
</div>
)}
</div>
);
};

View file

@ -0,0 +1,51 @@
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { browseFilesystemOptions } from "~/client/api-client/@tanstack/react-query.gen";
import { FileBrowser, type FileBrowserUiProps } from "~/client/components/file-browsers/file-browser";
import { useFileBrowser } from "~/client/hooks/use-file-browser";
import { parseError } from "~/client/lib/errors";
import { normalizeAbsolutePath } from "@zerobyte/core/utils";
import { logger } from "~/client/lib/logger";
type LocalFileBrowserProps = FileBrowserUiProps & {
initialPath?: string;
enabled?: boolean;
};
export const LocalFileBrowser = ({ initialPath = "/", enabled = true, ...uiProps }: LocalFileBrowserProps) => {
const queryClient = useQueryClient();
const normalizedInitialPath = normalizeAbsolutePath(initialPath);
const { data, isLoading, error } = useQuery({
...browseFilesystemOptions({ query: { path: normalizedInitialPath } }),
enabled,
});
const fileBrowser = useFileBrowser({
initialData: data,
isLoading,
fetchFolder: async (path) => {
return await queryClient.ensureQueryData(browseFilesystemOptions({ query: { path } }));
},
prefetchFolder: (path) => {
void queryClient.prefetchQuery(browseFilesystemOptions({ query: { path } })).catch((e) => logger.error(e));
},
});
return (
<FileBrowser
{...uiProps}
fileArray={fileBrowser.fileArray}
expandedFolders={fileBrowser.expandedFolders}
loadingFolders={fileBrowser.loadingFolders}
onFolderExpand={fileBrowser.handleFolderExpand}
onFolderHover={fileBrowser.handleFolderHover}
onLoadMore={fileBrowser.handleLoadMore}
getFolderPagination={fileBrowser.getFolderPagination}
isLoading={fileBrowser.isLoading}
isEmpty={fileBrowser.isEmpty}
errorMessage={parseError(error)?.message}
loadingMessage={uiProps.loadingMessage ?? "Loading directories..."}
emptyMessage={uiProps.emptyMessage ?? "No subdirectories found"}
/>
);
};

View file

@ -0,0 +1,157 @@
import { useCallback, useMemo } from "react";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { listSnapshotFilesOptions } from "~/client/api-client/@tanstack/react-query.gen";
import { FileBrowser, type FileBrowserUiProps } from "~/client/components/file-browsers/file-browser";
import { useFileBrowser } from "~/client/hooks/use-file-browser";
import { parseError } from "~/client/lib/errors";
import { isPathWithin, normalizeAbsolutePath } from "@zerobyte/core/utils";
function createPathPrefixFns(basePath: string) {
return {
strip(path: string) {
if (basePath === "/") return path;
if (path === basePath) return "/";
if (path.startsWith(`${basePath}/`)) return path.slice(basePath.length);
return path;
},
add(displayPath: string) {
if (basePath === "/") return displayPath;
if (displayPath === "/") return basePath;
return `${basePath}${displayPath}`;
},
};
}
type SnapshotTreeBrowserProps = FileBrowserUiProps & {
repositoryId: string;
snapshotId: string;
queryBasePath?: string;
displayBasePath?: string;
pageSize?: number;
enabled?: boolean;
onSingleSelectionKindChange?: (kind: "file" | "dir" | null) => void;
};
export const SnapshotTreeBrowser = (props: SnapshotTreeBrowserProps) => {
const {
repositoryId,
snapshotId,
queryBasePath = "/",
displayBasePath,
pageSize = 500,
enabled = true,
...uiProps
} = props;
const { selectedPaths, onSelectionChange, onSingleSelectionKindChange, ...fileBrowserUiProps } = uiProps;
const queryClient = useQueryClient();
const normalizedQueryBasePath = normalizeAbsolutePath(queryBasePath);
const normalizedDisplayBasePath = normalizeAbsolutePath(displayBasePath ?? "/");
const effectiveDisplayBasePath = isPathWithin(normalizedDisplayBasePath, normalizedQueryBasePath)
? normalizedDisplayBasePath
: "/";
const { data, isLoading, error } = useQuery({
...listSnapshotFilesOptions({
path: { shortId: repositoryId, snapshotId },
query: { path: normalizedQueryBasePath },
}),
enabled,
});
const displayPathFns = useMemo(() => createPathPrefixFns(effectiveDisplayBasePath), [effectiveDisplayBasePath]);
const displaySelectedPaths = useMemo(() => {
if (!selectedPaths) return undefined;
const displayPaths = new Set<string>();
for (const fullPath of selectedPaths) {
displayPaths.add(displayPathFns.strip(fullPath));
}
return displayPaths;
}, [displayPathFns, selectedPaths]);
const fileBrowser = useFileBrowser({
initialData: data,
isLoading,
fetchFolder: async (displayPath, offset = 0) => {
return await queryClient.ensureQueryData(
listSnapshotFilesOptions({
path: { shortId: repositoryId, snapshotId },
query: { path: displayPath, offset: offset, limit: pageSize },
}),
);
},
pathTransform: displayPathFns,
});
const displayPathKinds = useMemo(() => {
const kinds = new Map<string, "file" | "dir">();
for (const entry of fileBrowser.fileArray) {
kinds.set(entry.path, entry.type === "file" ? "file" : "dir");
let parentPath = entry.path;
while (true) {
const lastSlashIndex = parentPath.lastIndexOf("/");
if (lastSlashIndex <= 0) {
break;
}
parentPath = parentPath.slice(0, lastSlashIndex);
if (kinds.has(parentPath)) {
continue;
}
kinds.set(parentPath, "dir");
}
}
return kinds;
}, [fileBrowser.fileArray]);
const handleSelectionChange = useCallback(
(nextDisplayPaths: Set<string>) => {
if (!onSelectionChange) return;
const nextFullPaths = new Set<string>();
for (const displayPath of nextDisplayPaths) {
nextFullPaths.add(displayPathFns.add(displayPath));
}
if (onSingleSelectionKindChange) {
if (nextDisplayPaths.size === 1) {
const [selectedDisplayPath] = nextDisplayPaths;
if (selectedDisplayPath) {
onSingleSelectionKindChange(displayPathKinds.get(selectedDisplayPath) ?? null);
} else {
onSingleSelectionKindChange(null);
}
} else {
onSingleSelectionKindChange(null);
}
}
onSelectionChange(nextFullPaths);
},
[displayPathFns, displayPathKinds, onSelectionChange, onSingleSelectionKindChange],
);
return (
<FileBrowser
{...fileBrowserUiProps}
fileArray={fileBrowser.fileArray}
expandedFolders={fileBrowser.expandedFolders}
loadingFolders={fileBrowser.loadingFolders}
onFolderExpand={fileBrowser.handleFolderExpand}
onFolderHover={fileBrowser.handleFolderHover}
onLoadMore={fileBrowser.handleLoadMore}
getFolderPagination={fileBrowser.getFolderPagination}
isLoading={fileBrowser.isLoading}
isEmpty={fileBrowser.isEmpty}
errorMessage={parseError(error)?.message}
loadingMessage={fileBrowserUiProps.loadingMessage ?? "Loading files..."}
selectedPaths={displaySelectedPaths}
onSelectionChange={onSelectionChange ? handleSelectionChange : undefined}
/>
);
};

View file

@ -0,0 +1,61 @@
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { listFilesOptions } from "~/client/api-client/@tanstack/react-query.gen";
import { FileBrowser, type FileBrowserUiProps } from "~/client/components/file-browsers/file-browser";
import { useFileBrowser, type FetchFolderResult } from "~/client/hooks/use-file-browser";
import { parseError } from "~/client/lib/errors";
import { logger } from "~/client/lib/logger";
type VolumeFileBrowserProps = FileBrowserUiProps & {
volumeId: string;
enabled?: boolean;
};
export const VolumeFileBrowser = ({ volumeId, enabled = true, ...uiProps }: VolumeFileBrowserProps) => {
const queryClient = useQueryClient();
const { data, isLoading, error } = useQuery({
...listFilesOptions({ path: { shortId: volumeId } }),
enabled,
});
const fileBrowser = useFileBrowser({
initialData: data,
isLoading,
fetchFolder: async (path, offset): Promise<FetchFolderResult> => {
return await queryClient.ensureQueryData(
listFilesOptions({
path: { shortId: volumeId },
query: { path, offset: offset },
}),
);
},
prefetchFolder: (path) => {
void queryClient
.prefetchQuery(
listFilesOptions({
path: { shortId: volumeId },
query: { path },
}),
)
.catch((e) => logger.error(e));
},
});
return (
<FileBrowser
{...uiProps}
fileArray={fileBrowser.fileArray}
expandedFolders={fileBrowser.expandedFolders}
loadingFolders={fileBrowser.loadingFolders}
onFolderExpand={fileBrowser.handleFolderExpand}
onFolderHover={fileBrowser.handleFolderHover}
onLoadMore={fileBrowser.handleLoadMore}
getFolderPagination={fileBrowser.getFolderPagination}
isLoading={fileBrowser.isLoading}
isEmpty={fileBrowser.isEmpty}
errorMessage={parseError(error)?.message}
loadingMessage={uiProps.loadingMessage ?? "Loading files..."}
emptyMessage={uiProps.emptyMessage ?? "This volume appears to be empty."}
/>
);
};

View file

@ -8,10 +8,19 @@
* Original source: https://github.com/stackblitz/bolt.new
*/
import { ChevronDown, ChevronRight, File as FileIcon, Folder as FolderIcon, FolderOpen, Loader2 } from "lucide-react";
import {
ChevronDown,
ChevronRight,
File as FileIcon,
Folder as FolderIcon,
FolderOpen,
Loader2,
MoreHorizontal,
} from "lucide-react";
import { memo, type ReactNode, useCallback, useEffect, useMemo, useState } from "react";
import { cn } from "~/client/lib/utils";
import { Checkbox } from "~/client/components/ui/checkbox";
import { ByteSize } from "~/client/components/bytes-size";
const NODE_PADDING_LEFT = 12;
@ -23,12 +32,19 @@ export interface FileEntry {
modifiedAt?: number;
}
interface PaginationState {
hasMore: boolean;
isLoadingMore: boolean;
}
interface Props {
files?: FileEntry[];
selectedFile?: string;
onFileSelect?: (filePath: string) => void;
onFolderExpand?: (folderPath: string) => void;
onFolderHover?: (folderPath: string) => void;
onLoadMore?: (folderPath: string) => void;
getFolderPagination?: (folderPath: string) => PaginationState;
expandedFolders?: Set<string>;
loadingFolders?: Set<string>;
className?: string;
@ -48,6 +64,8 @@ export const FileTree = memo((props: Props) => {
selectedFile,
onFolderExpand,
onFolderHover,
onLoadMore,
getFolderPagination,
expandedFolders = new Set(),
loadingFolders = new Set(),
className,
@ -96,32 +114,39 @@ export const FileTree = memo((props: Props) => {
const toggleCollapseState = useCallback(
(fullPath: string) => {
const shouldExpand = collapsedFolders.has(fullPath);
setCollapsedFolders((prevSet) => {
const newSet = new Set(prevSet);
if (newSet.has(fullPath)) {
newSet.delete(fullPath);
onFolderExpand?.(fullPath);
} else {
newSet.add(fullPath);
}
return newSet;
});
if (shouldExpand) {
onFolderExpand?.(fullPath);
}
},
[onFolderExpand],
[collapsedFolders, onFolderExpand],
);
// Add new folders to collapsed set when file list changes
useEffect(() => {
setCollapsedFolders((prevSet) => {
let hasChanges = false;
const newSet = new Set(prevSet);
for (const item of fileList) {
if (item.kind === "folder" && !newSet.has(item.fullPath) && !expandedFolders.has(item.fullPath)) {
newSet.add(item.fullPath);
hasChanges = true;
}
}
return newSet;
return hasChanges ? newSet : prevSet;
});
}, [fileList, expandedFolders]);
@ -148,9 +173,9 @@ export const FileTree = memo((props: Props) => {
newSelection.add(path);
// Remove any descendants from selection since parent now covers them
for (const item of fileList) {
if (item.fullPath.startsWith(`${path}/`)) {
newSelection.delete(item.fullPath);
for (const selectedPath of newSelection) {
if (selectedPath.startsWith(`${path}/`)) {
newSelection.delete(selectedPath);
}
}
} else {
@ -181,7 +206,8 @@ export const FileTree = memo((props: Props) => {
if (
item.fullPath.startsWith(`${selectedParentPath}/`) &&
!item.fullPath.startsWith(`${path}/`) &&
item.fullPath !== path
item.fullPath !== path &&
!path.startsWith(`${item.fullPath}/`)
) {
newSelection.add(item.fullPath);
}
@ -189,39 +215,45 @@ export const FileTree = memo((props: Props) => {
}
}
const childrenByParent = new Map<string, string[]>();
for (const selectedPath of newSelection) {
const lastSlashIndex = selectedPath.lastIndexOf("/");
if (lastSlashIndex > 0) {
const parentPath = selectedPath.slice(0, lastSlashIndex);
if (!childrenByParent.has(parentPath)) {
childrenByParent.set(parentPath, []);
}
childrenByParent.get(parentPath)?.push(selectedPath);
}
}
// For each parent, check if all its children are selected
for (const [parentPath, selectedChildren] of childrenByParent.entries()) {
// Get all children of this parent from the file list
const allChildren = fileList.filter((item) => {
const itemParentPath = item.fullPath.slice(0, item.fullPath.lastIndexOf("/"));
return itemParentPath === parentPath;
});
// If all children are selected, replace them with the parent
if (allChildren.length > 0 && selectedChildren.length === allChildren.length) {
// Check that we have every child
const allChildrenPaths = new Set(allChildren.map((c) => c.fullPath));
const allChildrenSelected = selectedChildren.every((c) => allChildrenPaths.has(c));
if (allChildrenSelected) {
// Remove all children
for (const childPath of selectedChildren) {
newSelection.delete(childPath);
let changed = true;
while (changed) {
changed = false;
const childrenByParent = new Map<string, string[]>();
for (const selectedPath of newSelection) {
const lastSlashIndex = selectedPath.lastIndexOf("/");
if (lastSlashIndex > 0) {
const parentPath = selectedPath.slice(0, lastSlashIndex);
if (!childrenByParent.has(parentPath)) {
childrenByParent.set(parentPath, []);
}
childrenByParent.get(parentPath)?.push(selectedPath);
}
}
// For each parent, check if all its children are selected
for (const [parentPath, selectedChildren] of childrenByParent.entries()) {
// Get all children of this parent from the file list
const allChildren = fileList.filter((item) => {
const itemParentPath = item.fullPath.slice(0, item.fullPath.lastIndexOf("/"));
return itemParentPath === parentPath;
});
// If all children are selected, replace them with the parent
if (allChildren.length > 0 && selectedChildren.length === allChildren.length) {
// Check that we have every child
const allChildrenPaths = new Set(allChildren.map((c) => c.fullPath));
const allChildrenSelected = selectedChildren.every((c) => allChildrenPaths.has(c));
if (allChildrenSelected) {
// Remove all children
for (const childPath of selectedChildren) {
newSelection.delete(childPath);
}
// Add the parent
newSelection.add(parentPath);
changed = true;
break;
}
// Add the parent
newSelection.add(parentPath);
}
}
}
@ -282,12 +314,32 @@ export const FileTree = memo((props: Props) => {
[selectedPaths],
);
// Build a map of folder paths that need pagination to their last child's index
const folderPaginationMap = useMemo(() => {
const map = new Map<string, number>();
for (let i = 0; i < filteredFileList.length; i++) {
const item = filteredFileList[i];
const parentPath = item.fullPath.slice(0, item.fullPath.lastIndexOf("/")) || "/";
const pagination = getFolderPagination?.(parentPath);
if (pagination?.hasMore && !collapsedFolders.has(parentPath)) {
// Update the last index for this parent
map.set(parentPath, i);
}
}
return map;
}, [filteredFileList, getFolderPagination, collapsedFolders]);
return (
<div className={cn("text-sm", className)}>
{filteredFileList.map((fileOrFolder) => {
{filteredFileList.map((fileOrFolder, index) => {
const elements: React.ReactNode[] = [];
// Render the current file or folder
switch (fileOrFolder.kind) {
case "file": {
return (
elements.push(
<File
key={fileOrFolder.id}
selected={selectedFile === fileOrFolder.fullPath}
@ -296,11 +348,12 @@ export const FileTree = memo((props: Props) => {
withCheckbox={withCheckboxes}
checked={isPathSelected(fileOrFolder.fullPath)}
onCheckboxChange={handleSelectionChange}
/>
/>,
);
break;
}
case "folder": {
return (
elements.push(
<Folder
key={fileOrFolder.id}
folder={fileOrFolder}
@ -315,13 +368,31 @@ export const FileTree = memo((props: Props) => {
selectableMode={selectableFolders}
onFolderSelect={handleFolderSelect}
selected={selectedFolder === fileOrFolder.fullPath}
/>
/>,
);
}
default: {
return undefined;
break;
}
}
// Check if this is the last child of any folder with more files to load
for (const [folderPath, lastIndex] of folderPaginationMap.entries()) {
if (lastIndex === index) {
// This is the last loaded child of folderPath
const pagination = getFolderPagination?.(folderPath);
if (pagination?.hasMore) {
elements.push(
<LoadMoreButton
key={`load-more-${folderPath}`}
depth={fileOrFolder.depth}
onClick={() => onLoadMore?.(folderPath)}
isLoading={pagination.isLoadingMore}
/>,
);
}
}
}
return elements;
})}
</div>
);
@ -431,7 +502,7 @@ interface FileProps {
}
const File = memo(({ file, onFileSelect, selected, withCheckbox, checked, onCheckboxChange }: FileProps) => {
const { depth, name, fullPath } = file;
const { depth, name, fullPath, size } = file;
const handleClick = useCallback(() => {
onFileSelect(fullPath);
@ -458,6 +529,36 @@ const File = memo(({ file, onFileSelect, selected, withCheckbox, checked, onChec
<Checkbox checked={checked} onCheckedChange={handleCheckboxChange} onClick={(e) => e.stopPropagation()} />
)}
<span className="truncate">{name}</span>
{typeof size === "number" && (
<span className="ml-auto shrink-0 text-xs text-muted-foreground">
<ByteSize bytes={size} base={1024} />
</span>
)}
</NodeButton>
);
});
interface LoadMoreButtonProps {
depth: number;
onClick: () => void;
isLoading?: boolean;
}
const LoadMoreButton = memo(({ depth, onClick, isLoading }: LoadMoreButtonProps) => {
return (
<NodeButton
depth={depth}
className="text-muted-foreground hover:bg-accent/50 cursor-pointer"
icon={
isLoading ? (
<Loader2 className="w-4 h-4 shrink-0 animate-spin" />
) : (
<MoreHorizontal className="w-4 h-4 shrink-0" />
)
}
onClick={onClick}
>
<span className="text-xs">{isLoading ? "Loading more..." : "Load more files"}</span>
</NodeButton>
);
});
@ -474,17 +575,30 @@ interface ButtonProps {
const NodeButton = memo(({ depth, icon, onClick, onMouseEnter, className, children }: ButtonProps) => {
const paddingLeft = useMemo(() => `${8 + depth * NODE_PADDING_LEFT}px`, [depth]);
const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
onClick?.();
}
},
[onClick],
);
return (
<button
type="button"
<div
// oxlint-disable-next-line jsx_a11y/prefer-tag-over-role
role="button"
tabIndex={0}
className={cn("flex items-center gap-2 w-full pr-2 text-sm py-1.5 text-left", className)}
style={{ paddingLeft }}
onClick={onClick}
onMouseEnter={onMouseEnter}
onKeyDown={handleKeyDown}
>
{icon}
<div className="truncate w-full flex items-center gap-2">{children}</div>
</button>
</div>
);
});
@ -499,6 +613,7 @@ interface BaseNode {
interface FileNode extends BaseNode {
kind: "file";
size?: number;
}
interface FolderNode extends BaseNode {
@ -517,13 +632,46 @@ function buildFileList(files: FileEntry[], foldersOnly = false): Node[] {
const depth = segments.length - 1;
const name = segments[segments.length - 1];
if (!name) {
continue;
}
if (!fileMap.has(file.path)) {
const isFile = file.type === "file";
fileMap.set(file.path, {
kind: file.type === "file" ? "file" : "folder",
kind: isFile ? "file" : "folder",
id: fileMap.size,
name,
fullPath: file.path,
depth,
size: file.size,
});
}
let parentPath = file.path;
while (true) {
const lastSlashIndex = parentPath.lastIndexOf("/");
if (lastSlashIndex <= 0) {
break;
}
parentPath = parentPath.slice(0, lastSlashIndex);
if (fileMap.has(parentPath)) {
continue;
}
const parentSegments = parentPath.split("/").filter((segment) => segment);
const parentName = parentSegments[parentSegments.length - 1];
if (!parentName) {
continue;
}
fileMap.set(parentPath, {
kind: "folder",
id: fileMap.size,
name: parentName,
fullPath: parentPath,
depth: parentSegments.length - 1,
});
}
}
@ -592,5 +740,7 @@ function compareNodes(a: Node, b: Node): number {
return a.kind === "folder" ? -1 : 1;
}
return a.name.localeCompare(b.name, undefined, { numeric: true, sensitivity: "base" });
if (a.name < b.name) return -1;
if (a.name > b.name) return 1;
return 0;
}

View file

@ -9,16 +9,17 @@ interface GridBackgroundProps {
export function GridBackground({ children, className, containerClassName }: GridBackgroundProps) {
return (
<div
className={cn(
"relative min-h-full w-full overflow-x-hidden",
"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)]",
"dark:bg-[linear-gradient(to_right,#262626_1px,transparent_1px),linear-gradient(to_bottom,#262626_1px,transparent_1px)]",
containerClassName,
)}
>
<div className={cn("relative container m-auto", className)}>{children}</div>
<div className={cn("relative min-h-full w-full", containerClassName)}>
<div
className={cn(
"pointer-events-none absolute inset-0 w-full h-full",
"bg-[size:40px_40px]",
"bg-[linear-gradient(to_right,rgba(0,0,0,0.04)_1px,transparent_1px),linear-gradient(to_bottom,rgba(0,0,0,0.04)_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%)]",
)}
/>
<div className={cn("relative container m-auto z-10", className)}>{children}</div>
</div>
);
}

View file

@ -1,80 +1,89 @@
import { useMutation } from "@tanstack/react-query";
import { LifeBuoy } from "lucide-react";
import { Outlet, redirect, useNavigate } from "react-router";
import { LifeBuoy, LogOut } from "lucide-react";
import { toast } from "sonner";
import { appContext } from "~/context";
import { authMiddleware } from "~/middleware/auth";
import type { Route } from "./+types/layout";
import { AppBreadcrumb } from "./app-breadcrumb";
import { type AppContext } from "~/context";
import { GridBackground } from "./grid-background";
import { Button } from "./ui/button";
import { Button, buttonVariants } from "./ui/button";
import { SidebarProvider, SidebarTrigger } from "./ui/sidebar";
import { AppSidebar } from "./app-sidebar";
import { logoutMutation } from "../api-client/@tanstack/react-query.gen";
import { authClient } from "../lib/auth-client";
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";
export const clientMiddleware = [authMiddleware];
type Props = {
loaderData: AppContext;
};
export async function clientLoader({ context }: Route.LoaderArgs) {
const ctx = context.get(appContext);
if (ctx.user && !ctx.user.hasDownloadedResticPassword) {
throw redirect("/download-recovery-key");
}
return ctx;
}
export default function Layout({ loaderData }: Route.ComponentProps) {
export function Layout({ loaderData }: Props) {
const navigate = useNavigate();
const logout = useMutation({
...logoutMutation(),
onSuccess: async () => {
navigate("/login", { replace: true });
},
onError: (error) => {
console.error(error);
toast.error("Logout failed", { description: error.message });
},
});
const handleLogout = async () => {
await authClient.signOut({
fetchOptions: {
onSuccess: () => {
void navigate({ to: "/login", replace: true });
},
onError: ({ error }) => {
toast.error("Logout failed", { description: error.message });
},
},
});
};
return (
<SidebarProvider defaultOpen={true}>
<AppSidebar />
<div className="w-full relative flex flex-col h-screen overflow-hidden">
<header className="z-50 bg-card-header border-b border-border/50 shrink-0">
<div className="flex items-center justify-between py-3 sm:py-4 px-2 sm:px-8 mx-auto container">
<div className="flex items-center gap-4">
<SidebarProvider defaultOpen={loaderData.sidebarOpen}>
<AppSidebar isInstanceAdmin={loaderData.user?.role === "admin"} />
<div className="w-full relative flex flex-col min-h-screen md:h-screen md:overflow-hidden">
<header className="z-50 bg-card-header border-b border-border/80 dark:border-border/50 shrink-0 h-16.25">
<div className="flex items-center h-full justify-between px-2 sm:px-8 mx-auto container gap-4">
<div className="flex items-center gap-4 min-w-0">
<SidebarTrigger />
<AppBreadcrumb />
</div>
{loaderData.user && (
<div className="flex items-center gap-4">
<span className="text-sm text-muted-foreground hidden md:inline-flex">
Welcome,&nbsp;
<span className="text-strong-accent">{loaderData.user?.username}</span>
<div className="flex items-center bg-card dark:bg-muted/30 border border-border/80 dark:border-border/50 px-2 py-1 rounded-full shadow-[0_2px_10px_-4px_rgba(0,0,0,0.05)] dark:shadow-sm">
<span className="text-sm text-muted-foreground hidden md:inline-flex pl-2 mr-5">
<span className="text-foreground">{loaderData.user.name}</span>
</span>
<Button variant="default" size="sm" onClick={() => logout.mutate({})} loading={logout.isPending}>
Logout
</Button>
<Button variant="default" size="sm" className="relative overflow-hidden hidden lg:inline-flex">
<a
href="https://github.com/nicotsx/ironmount/issues/new"
target="_blank"
rel="noreferrer"
className="flex items-center gap-2"
>
<span className="flex items-center gap-2">
<LifeBuoy />
<span>Report an issue</span>
</span>
</a>
</Button>
<ThemeToggle />
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="rounded-full h-7 text-xs text-muted-foreground hover:text-foreground"
onClick={handleLogout}
>
<LogOut className="w-4 h-4" />
</Button>
</TooltipTrigger>
<TooltipContent>Logout</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<a
href="https://github.com/nicotsx/zerobyte/issues/new/choose"
target="_blank"
rel="noreferrer"
className={buttonVariants({
variant: "ghost",
size: "icon",
className:
"relative overflow-hidden hidden lg:inline-flex rounded-full h-7 w-7 text-muted-foreground hover:text-foreground",
})}
>
<LifeBuoy className="w-4 h-4" />
</a>
</TooltipTrigger>
<TooltipContent>Report an issue</TooltipContent>
</Tooltip>
</div>
)}
</div>
</header>
<div className="main-content flex-1 overflow-y-auto">
<div className="main-content flex-1 md:overflow-y-auto">
<GridBackground>
<main className="flex flex-col p-2 pb-6 pt-2 sm:p-8 sm:pt-6 mx-auto">
<Outlet />
@ -82,6 +91,7 @@ export default function Layout({ loaderData }: Route.ComponentProps) {
</GridBackground>
</div>
</div>
<DevPanelListener />
</SidebarProvider>
);
}

View file

@ -0,0 +1,23 @@
import { Badge } from "~/client/components/ui/badge";
import { Tooltip, TooltipContent, TooltipTrigger } from "~/client/components/ui/tooltip";
type ManagedBadgeProps = {
label?: string;
message?: string;
};
const defaultMessage =
"This resource is provisioned at startup. Changes are useful for testing, but the next provisioning sync can overwrite or recreate it.";
export const ManagedBadge = ({ label = "Managed", message = defaultMessage }: ManagedBadgeProps) => {
return (
<Tooltip>
<TooltipTrigger asChild>
<Badge variant="secondary">{label}</Badge>
</TooltipTrigger>
<TooltipContent>
<p className="max-w-80 text-sm">{message}</p>
</TooltipContent>
</Tooltip>
);
};

View file

@ -15,7 +15,7 @@ export const OnOff = ({ isOn, toggle, enabledLabel, disabledLabel, disabled }: P
className={cn(
"flex items-center gap-2 rounded-full border px-3 py-1.5 text-xs font-semibold uppercase tracking-wide transition-colors",
isOn
? "border-emerald-200 bg-emerald-50 text-emerald-700 dark:border-emerald-500/40 dark:bg-emerald-500/10 dark:text-emerald-200"
? "border-success/30 bg-success/10 text-success"
: "border-muted bg-muted/40 text-muted-foreground dark:border-muted/60 dark:bg-muted/10",
)}
>

View file

@ -0,0 +1,110 @@
import { ChevronsUpDown } from "lucide-react";
import { toast } from "sonner";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from "~/client/components/ui/dropdown-menu";
import { SidebarMenu, SidebarMenuButton, SidebarMenuItem, useSidebar } from "~/client/components/ui/sidebar";
import { authClient } from "~/client/lib/auth-client";
import { useMutation } from "@tanstack/react-query";
import { useOrganizationContext } from "../hooks/use-org-context";
function getOrganizationInitials(name?: string): string {
const trimmedName = name?.trim();
if (!trimmedName) {
return "O";
}
return trimmedName
.split(/\s+/)
.slice(0, 2)
.map((part) => part.charAt(0).toUpperCase())
.join("");
}
export function OrganizationSwitcher() {
const { isMobile } = useSidebar();
const { organizations, activeOrganization } = useOrganizationContext();
const switchOrganizationMutation = useMutation({
mutationFn: async (organizationId: string) => {
const { error } = await authClient.organization.setActive({ organizationId });
if (error) throw new Error(error.message);
},
onError: (error) => {
const message = error instanceof Error ? error.message : "Unexpected error while switching organizations";
toast.error("Failed to switch organization", { description: message });
},
});
if (organizations === undefined) {
return null;
}
if (organizations.length <= 1) {
return null;
}
return (
<SidebarMenu className="mb-3">
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuButton
size="lg"
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
<div className="bg-foreground text-sidebar-primary-foreground flex aspect-square size-8 shrink-0 items-center justify-center overflow-hidden rounded-lg">
{activeOrganization?.logo ? (
<img
src={activeOrganization.logo}
alt={`${activeOrganization.name} logo`}
className="size-full object-cover"
/>
) : (
<span className="text-xs font-semibold">{getOrganizationInitials(activeOrganization?.name)}</span>
)}
</div>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">{activeOrganization?.name}</span>
<span className="truncate text-xs">{organizations.length} organizations</span>
</div>
<ChevronsUpDown className="ml-auto group-data-[collapsible=icon]:hidden" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
align="start"
side={isMobile ? "bottom" : "right"}
sideOffset={4}
>
<DropdownMenuLabel className="text-muted-foreground text-xs">Organizations</DropdownMenuLabel>
{organizations.map((organization) => (
<DropdownMenuItem
key={organization.id}
onClick={() => switchOrganizationMutation.mutate(organization.id)}
className="gap-2 p-2"
disabled={switchOrganizationMutation.isPending}
>
<div className="flex size-6 shrink-0 items-center justify-center overflow-hidden rounded-md border">
{organization.logo ? (
<img src={organization.logo} alt={`${organization.name} logo`} className="size-full object-cover" />
) : (
<span className="text-[10px] font-semibold">{getOrganizationInitials(organization.name)}</span>
)}
</div>
<span className="min-w-0 flex-1 truncate">{organization.name}</span>
<DropdownMenuShortcut>{organization.id === activeOrganization?.id && "Current"}</DropdownMenuShortcut>
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
);
}

View file

@ -0,0 +1,39 @@
import { useState } from "react";
import { DirectoryBrowser } from "./file-browsers/directory-browser";
import { Button } from "./ui/button";
type Props = {
value: string;
onChange: (path: string) => void;
label?: string;
};
export const PathSelector = ({ value, onChange }: Props) => {
const [showBrowser, setShowBrowser] = useState(false);
if (showBrowser) {
return (
<div className="space-y-2">
<DirectoryBrowser
selectedPath={value}
onSelectPath={(path) => {
onChange(path);
setShowBrowser(false);
}}
/>
<Button type="button" variant="ghost" size="sm" onClick={() => setShowBrowser(false)}>
Cancel
</Button>
</div>
);
}
return (
<div className="flex items-center gap-2">
<div className="flex-1 text-sm font-mono bg-muted px-3 py-2 rounded-md border">{value}</div>
<Button type="button" variant="outline" onClick={() => setShowBrowser(true)} size="sm">
Change
</Button>
</div>
);
};

View file

@ -0,0 +1,46 @@
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "~/client/components/ui/dialog";
import { ScrollArea } from "~/client/components/ui/scroll-area";
import { useTimeFormat } from "~/client/lib/datetime";
import type { UpdateInfoDto } from "~/server/modules/system/system.dto";
interface ReleaseNotesDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
updates: UpdateInfoDto | undefined;
}
export function ReleaseNotesDialog({ open, onOpenChange, updates }: ReleaseNotesDialogProps) {
const { formatDate } = useTimeFormat();
if (!updates) return null;
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-h-[80vh] flex flex-col p-0">
<DialogHeader className="p-6 pb-2">
<DialogTitle className="flex items-center gap-2">Release Notes</DialogTitle>
<DialogDescription>
Current version: {updates.currentVersion} Latest version: {updates.latestVersion}
</DialogDescription>
</DialogHeader>
<ScrollArea className="p-6 pt-2 h-[50vh] min-w-0">
<div className="space-y-8">
{updates.missedReleases.map((release) => (
<div key={release.version} className="space-y-4">
<div className="flex items-center justify-between border-b pb-2">
<h3 className="text-lg font-bold text-foreground">{release.version}</h3>
<span className="text-sm text-muted-foreground">{formatDate(release.publishedAt)}</span>
</div>
<div className="prose prose-sm dark:prose-invert max-w-none prose-pre:bg-muted prose-pre:text-muted-foreground prose-a:text-primary hover:prose-a:underline wrap-anywhere text-wrap prose-pre:whitespace-pre-wrap prose-pre:wrap-anywhere">
<ReactMarkdown remarkPlugins={[remarkGfm]}>{release.body}</ReactMarkdown>
</div>
</div>
))}
</div>
</ScrollArea>
</DialogContent>
</Dialog>
);
}

View file

@ -1,5 +1,5 @@
import { Database, HardDrive, Cloud, Server } from "lucide-react";
import type { RepositoryBackend } from "~/schemas/restic";
import type { RepositoryBackend } from "@zerobyte/core/restic";
type Props = {
backend: RepositoryBackend;
@ -15,6 +15,7 @@ export const RepositoryIcon = ({ backend, className = "h-4 w-4" }: Props) => {
case "gcs":
return <Cloud className={className} />;
case "rest":
case "sftp":
return <Server className={className} />;
default:
return <Database className={className} />;

View file

@ -0,0 +1,426 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { useMutation } from "@tanstack/react-query";
import { AlertTriangle, ChevronDown, Download, FolderOpen, RotateCcw } from "lucide-react";
import { Button } from "~/client/components/ui/button";
import { Tooltip, TooltipContent, TooltipTrigger } from "~/client/components/ui/tooltip";
import { Alert, AlertDescription, AlertTitle } from "~/client/components/ui/alert";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card";
import { Input } from "~/client/components/ui/input";
import { Label } from "~/client/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/client/components/ui/select";
import {
AlertDialog,
AlertDialogAction,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "~/client/components/ui/alert-dialog";
import { PathSelector } from "~/client/components/path-selector";
import { SnapshotTreeBrowser } from "~/client/components/file-browsers/snapshot-tree-browser";
import { RestoreProgress } from "~/client/components/restore-progress";
import { restoreSnapshotMutation } from "~/client/api-client/@tanstack/react-query.gen";
import { type RestoreCompletedEvent, useServerEvents } from "~/client/hooks/use-server-events";
import { OVERWRITE_MODES, type OverwriteMode } from "@zerobyte/core/restic";
import { isPathWithin } from "@zerobyte/core/utils";
import type { Repository } from "~/client/lib/types";
import { handleRepositoryError } from "~/client/lib/errors";
import { useNavigate } from "@tanstack/react-router";
import { cn } from "~/client/lib/utils";
type RestoreLocation = "original" | "custom";
interface RestoreFormProps {
repository: Repository;
snapshotId: string;
returnPath: string;
queryBasePath?: string;
displayBasePath?: string;
hasNonPosixSnapshotPaths?: boolean;
}
export function RestoreForm({
repository,
snapshotId,
returnPath,
queryBasePath,
displayBasePath,
hasNonPosixSnapshotPaths = false,
}: RestoreFormProps) {
const navigate = useNavigate();
const { addEventListener } = useServerEvents();
const snapshotBasePath = queryBasePath ?? "/";
const hasMismatchedDisplayBasePath = displayBasePath && !isPathWithin(displayBasePath, snapshotBasePath);
const restoreRequiresCustomTarget = hasNonPosixSnapshotPaths || hasMismatchedDisplayBasePath;
const [restoreLocation, setRestoreLocation] = useState<RestoreLocation>(
restoreRequiresCustomTarget ? "custom" : "original",
);
const [customTargetPath, setCustomTargetPath] = useState("");
const [overwriteMode, setOverwriteMode] = useState<OverwriteMode>("always");
const [showAdvanced, setShowAdvanced] = useState(false);
const [excludeXattr, setExcludeXattr] = useState("");
const [isRestoreActive, setIsRestoreActive] = useState(false);
const [restoreResult, setRestoreResult] = useState<RestoreCompletedEvent | null>(null);
const [showRestoreResultAlert, setShowRestoreResultAlert] = useState(false);
const restoreCompletedRef = useRef(false);
const [selectedPaths, setSelectedPaths] = useState<Set<string>>(new Set());
const [selectedPathKind, setSelectedPathKind] = useState<"file" | "dir" | null>(null);
const trimmedCustomTargetPath = customTargetPath.trim();
const hasCustomTargetPath = trimmedCustomTargetPath !== "";
const selectedPathCount = selectedPaths.size;
useEffect(() => {
if (restoreRequiresCustomTarget) {
setRestoreLocation("custom");
}
}, [restoreRequiresCustomTarget]);
useEffect(() => {
const abortController = new AbortController();
const signal = abortController.signal;
addEventListener(
"restore:started",
(startedData) => {
if (startedData.repositoryId === repository.shortId && startedData.snapshotId === snapshotId) {
restoreCompletedRef.current = false;
setIsRestoreActive(true);
setRestoreResult(null);
setShowRestoreResultAlert(false);
}
},
{ signal },
);
addEventListener(
"restore:progress",
(progressData) => {
if (progressData.repositoryId === repository.shortId && progressData.snapshotId === snapshotId) {
if (restoreCompletedRef.current) {
return;
}
setIsRestoreActive(true);
}
},
{ signal },
);
addEventListener(
"restore:completed",
(completedData) => {
if (completedData.repositoryId === repository.shortId && completedData.snapshotId === snapshotId) {
restoreCompletedRef.current = true;
setIsRestoreActive(false);
setRestoreResult(completedData);
setShowRestoreResultAlert(true);
}
},
{ signal },
);
return () => {
abortController.abort();
};
}, [addEventListener, repository.shortId, snapshotId]);
const { mutate: restoreSnapshot, isPending: isRestoring } = useMutation({
...restoreSnapshotMutation(),
onError: (error) => {
restoreCompletedRef.current = true;
setIsRestoreActive(false);
handleRepositoryError("Restore failed", error, repository.shortId);
},
});
const handleRestore = useCallback(() => {
const excludeXattrValues = excludeXattr
.split(",")
.map((value) => value.trim())
.filter(Boolean);
const isCustomLocation = restoreLocation === "custom";
const targetPath = isCustomLocation && hasCustomTargetPath ? trimmedCustomTargetPath : undefined;
const includePaths = Array.from(selectedPaths);
restoreCompletedRef.current = false;
setIsRestoreActive(true);
setRestoreResult(null);
setShowRestoreResultAlert(false);
restoreSnapshot({
path: { shortId: repository.shortId },
body: {
snapshotId,
include: includePaths.length > 0 ? includePaths : undefined,
selectedItemKind: includePaths.length === 1 ? (selectedPathKind ?? undefined) : undefined,
excludeXattr: excludeXattrValues.length > 0 ? excludeXattrValues : undefined,
targetPath,
overwrite: overwriteMode,
},
});
}, [
repository.shortId,
snapshotId,
excludeXattr,
hasCustomTargetPath,
restoreLocation,
trimmedCustomTargetPath,
selectedPaths,
selectedPathKind,
overwriteMode,
restoreSnapshot,
]);
const handleDownload = useCallback(() => {
if (selectedPaths.size > 1) return;
const url = new URL(
`/api/v1/repositories/${repository.shortId}/snapshots/${snapshotId}/dump`,
window.location.origin,
);
const [selectedPath] = selectedPaths;
if (selectedPath) {
url.searchParams.set("path", selectedPath);
if (selectedPathKind) {
url.searchParams.set("kind", selectedPathKind);
}
}
window.location.assign(url.toString());
}, [repository.shortId, snapshotId, selectedPathKind, selectedPaths]);
const acknowledgeRestoreResult = useCallback(() => {
setShowRestoreResultAlert(false);
setRestoreResult(null);
}, []);
const handleResultAlertOpenChange = useCallback((open: boolean) => {
if (open) {
setShowRestoreResultAlert(true);
}
}, []);
const canRestore = restoreRequiresCustomTarget
? hasCustomTargetPath
: restoreLocation === "original" || hasCustomTargetPath;
const canDownload = selectedPathCount <= 1;
const isRestoreRunning = isRestoring || isRestoreActive;
function getDownloadButtonText(): string {
if (selectedPathCount > 0) {
return `Download ${selectedPathCount} ${selectedPathCount === 1 ? "item" : "items"}`;
}
return "Download All";
}
function getRestoreButtonText(): string {
if (isRestoreRunning) {
return "Restoring...";
}
if (selectedPathCount > 0) {
return `Restore ${selectedPathCount} ${selectedPathCount === 1 ? "item" : "items"}`;
}
return "Restore All";
}
return (
<div className="space-y-6">
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
<div>
<h1 className="text-2xl font-bold">Restore Snapshot</h1>
<p className="text-sm text-muted-foreground">
{repository.name} / {snapshotId}
</p>
</div>
<div className="flex flex-wrap gap-2">
<Button variant="outline" onClick={() => navigate({ to: returnPath })}>
Cancel
</Button>
<Tooltip>
<TooltipTrigger asChild>
<span className="inline-flex">
<Button variant="outline" onClick={handleDownload} disabled={!canDownload}>
<Download className="h-4 w-4 mr-2" />
{getDownloadButtonText()}
</Button>
</span>
</TooltipTrigger>
<TooltipContent className={cn({ hidden: canDownload })}>
<p>Download is available only for one selected item, or with no selection to download everything.</p>
</TooltipContent>
</Tooltip>
<Button variant="primary" onClick={handleRestore} disabled={isRestoreRunning || !canRestore}>
<RotateCcw className="h-4 w-4 mr-2" />
{getRestoreButtonText()}
</Button>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="space-y-6">
{isRestoreRunning && <RestoreProgress repositoryId={repository.shortId} snapshotId={snapshotId} />}
{restoreRequiresCustomTarget && (
<Alert variant="warning">
<AlertTriangle className="size-4" />
<AlertTitle>Source paths do not match</AlertTitle>
<AlertDescription>
This snapshot was created from source paths that do not match this Zerobyte server or the current linked
volume. Restoring to the original location is unavailable. Restore it to a custom location, or download
it instead.
</AlertDescription>
</Alert>
)}
<Card>
<CardHeader>
<CardTitle>Restore Location</CardTitle>
<CardDescription>Choose where to restore the files</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 gap-2">
<Button
type="button"
variant={restoreLocation === "original" ? "secondary" : "outline"}
size="sm"
className="flex justify-start gap-2"
onClick={() => setRestoreLocation("original")}
disabled={!!restoreRequiresCustomTarget}
>
<RotateCcw size={16} className="mr-1" />
Original location
</Button>
<Button
type="button"
variant={restoreLocation === "custom" ? "secondary" : "outline"}
size="sm"
className="justify-start gap-2"
onClick={() => setRestoreLocation("custom")}
>
<FolderOpen size={16} className="mr-1" />
Custom location
</Button>
</div>
{restoreLocation === "custom" && (
<div className="space-y-2">
<PathSelector value={customTargetPath || "/"} onChange={setCustomTargetPath} />
<p className="text-xs text-muted-foreground">Files will be restored directly to this path</p>
</div>
)}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Overwrite Mode</CardTitle>
<CardDescription>How to handle existing files</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
<Select value={overwriteMode} onValueChange={(value) => setOverwriteMode(value as OverwriteMode)}>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select overwrite behavior" />
</SelectTrigger>
<SelectContent>
<SelectItem value={OVERWRITE_MODES.always}>Always overwrite</SelectItem>
<SelectItem value={OVERWRITE_MODES.ifChanged}>Only if content changed</SelectItem>
<SelectItem value={OVERWRITE_MODES.ifNewer}>Only if snapshot is newer</SelectItem>
<SelectItem value={OVERWRITE_MODES.never}>Never overwrite</SelectItem>
</SelectContent>
</Select>
<p className="text-xs text-muted-foreground">
{overwriteMode === OVERWRITE_MODES.always &&
"Existing files will always be replaced with the snapshot version."}
{overwriteMode === OVERWRITE_MODES.ifChanged &&
"Files are only replaced if their content differs from the snapshot."}
{overwriteMode === OVERWRITE_MODES.ifNewer &&
"Files are only replaced if the snapshot version has a newer modification time."}
{overwriteMode === OVERWRITE_MODES.never &&
"Existing files will never be replaced, only missing files are restored."}
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="cursor-pointer" onClick={() => setShowAdvanced(!showAdvanced)}>
<div className="flex items-center justify-between">
<CardTitle className="text-base">Advanced options</CardTitle>
<ChevronDown size={16} className={`transition-transform ${showAdvanced ? "rotate-180" : ""}`} />
</div>
</CardHeader>
{showAdvanced && (
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="exclude-xattr" className="text-sm">
Exclude extended attributes
</Label>
<Input
id="exclude-xattr"
placeholder="com.apple.metadata,user.*,nfs4.*"
value={excludeXattr}
onChange={(e) => setExcludeXattr(e.target.value)}
/>
<p className="text-xs text-muted-foreground">
Exclude specific extended attributes during restore (comma-separated)
</p>
</div>
</CardContent>
)}
</Card>
</div>
<Card className="lg:col-span-2 flex flex-col">
<CardHeader>
<CardTitle>Select Files to Restore</CardTitle>
<CardDescription>
{selectedPaths.size > 0
? `${selectedPaths.size} ${selectedPaths.size === 1 ? "item" : "items"} selected`
: "Select specific files or folders, or leave empty to restore everything"}
</CardDescription>
</CardHeader>
<CardContent className="flex-1 overflow-hidden flex flex-col p-0">
<SnapshotTreeBrowser
repositoryId={repository.shortId}
snapshotId={snapshotId}
queryBasePath={snapshotBasePath}
displayBasePath={displayBasePath}
pageSize={500}
className="flex flex-1 min-h-0 flex-col"
treeContainerClassName="overflow-auto flex-1 min-h-0 border border-border rounded-md bg-card m-4"
treeClassName="px-2 py-2"
loadingMessage="Loading files..."
emptyMessage="No files in this snapshot"
withCheckboxes
selectedPaths={selectedPaths}
onSelectionChange={setSelectedPaths}
onSingleSelectionKindChange={setSelectedPathKind}
stateClassName="flex-1 min-h-0"
/>
</CardContent>
</Card>
</div>
<AlertDialog open={showRestoreResultAlert} onOpenChange={handleResultAlertOpenChange}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
{restoreResult?.status === "success" ? "Restore completed" : "Restore failed"}
</AlertDialogTitle>
<AlertDialogDescription>
{restoreResult?.status === "success"
? `Snapshot ${snapshotId} was restored successfully.`
: restoreResult?.error || `Snapshot ${snapshotId} could not be restored.`}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogAction onClick={acknowledgeRestoreResult}>OK</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
}

View file

@ -0,0 +1,101 @@
import { useEffect, useState } from "react";
import { ByteSize } from "~/client/components/bytes-size";
import { useFormatBytes } from "~/client/hooks/use-format-bytes";
import { useRootLoaderData } from "~/client/hooks/use-root-loader-data";
import { Card } from "~/client/components/ui/card";
import { Progress } from "~/client/components/ui/progress";
import { type RestoreProgressEvent, useServerEvents } from "~/client/hooks/use-server-events";
import { formatDuration } from "~/utils/utils";
type Props = {
repositoryId: string;
snapshotId: string;
};
export const RestoreProgress = ({ repositoryId, snapshotId }: Props) => {
const formatBytes = useFormatBytes();
const { locale } = useRootLoaderData();
const { addEventListener } = useServerEvents();
const [progress, setProgress] = useState<RestoreProgressEvent | null>(null);
useEffect(() => {
const abortController = new AbortController();
addEventListener(
"restore:progress",
(progressData) => {
if (progressData.repositoryId === repositoryId && progressData.snapshotId === snapshotId) {
setProgress(progressData);
}
},
{ signal: abortController.signal },
);
addEventListener(
"restore:completed",
(completedData) => {
if (completedData.repositoryId === repositoryId && completedData.snapshotId === snapshotId) {
setProgress(null);
}
},
{ signal: abortController.signal },
);
return () => abortController.abort();
}, [addEventListener, repositoryId, snapshotId]);
if (!progress) {
return (
<Card className="p-4">
<div className="flex items-center gap-2">
<div className="h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent" />
<span className="font-medium">Restore in progress</span>
</div>
</Card>
);
}
const percentDone = Math.round(progress.percent_done * 100);
const speed = progress.seconds_elapsed > 0 ? formatBytes(progress.bytes_restored / progress.seconds_elapsed) : null;
return (
<Card className="p-4">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<div className="h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent" />
<span className="font-medium">Restore in progress</span>
</div>
<span className="text-sm font-medium text-primary">{percentDone}%</span>
</div>
<Progress value={percentDone} className="h-2 mb-4" />
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<p className="text-xs uppercase text-muted-foreground">Files</p>
<p className="font-medium">
{progress.files_restored.toLocaleString(locale)}
&nbsp;/&nbsp;
{progress.total_files.toLocaleString(locale)}
</p>
</div>
<div>
<p className="text-xs uppercase text-muted-foreground">Data</p>
<p className="font-medium">
<ByteSize bytes={progress.bytes_restored} base={1024} />
&nbsp;/&nbsp;
<ByteSize bytes={progress.total_bytes} base={1024} />
</p>
</div>
<div>
<p className="text-xs uppercase text-muted-foreground">Elapsed</p>
<p className="font-medium">{formatDuration(progress.seconds_elapsed)}</p>
</div>
<div>
<p className="text-xs uppercase text-muted-foreground">Speed</p>
<p className="font-medium">{speed ? `${speed.text} ${speed.unit}/s` : "Calculating..."}</p>
</div>
</div>
</Card>
);
};

View file

@ -0,0 +1,89 @@
import { useState } from "react";
import { cn } from "~/client/lib/utils";
import { HoverCard, HoverCardContent, HoverCardTrigger } from "./ui/hover-card";
interface RetentionCategoryBadgesProps {
categories: string[];
className?: string;
}
const categoryColors: Record<string, string> = {
last: "bg-blue-500/20 text-blue-700 dark:text-blue-300 border-blue-500/30",
hourly: "bg-cyan-500/20 text-cyan-700 dark:text-cyan-300 border-cyan-500/30",
daily: "bg-green-500/20 text-green-700 dark:text-green-300 border-green-500/30",
weekly: "bg-orange-500/20 text-orange-700 dark:text-orange-300 border-orange-500/30",
monthly: "bg-purple-500/20 text-purple-700 dark:text-purple-300 border-purple-500/30",
yearly: "bg-red-500/20 text-red-700 dark:text-red-300 border-red-500/30",
};
const categoryLabels: Record<string, string> = {
last: "Last",
hourly: "Hourly",
daily: "Daily",
weekly: "Weekly",
monthly: "Monthly",
yearly: "Yearly",
};
function Badge({ category }: { category: string }) {
return (
<span
className={cn(
"inline-flex items-center px-2 py-0.5 rounded text-xs font-medium border",
categoryColors[category] || "bg-gray-500/20 text-gray-700 dark:text-gray-300 border-gray-500/30",
)}
>
{categoryLabels[category] || category}
</span>
);
}
export function RetentionCategoryBadges({ categories, className }: RetentionCategoryBadgesProps) {
const [open, setOpen] = useState(false);
if (categories.length === 0) {
return null;
}
const order = ["last", "hourly", "daily", "weekly", "monthly", "yearly"];
const sortedCategories = [...categories].sort((a, b) => {
const indexA = order.indexOf(a);
const indexB = order.indexOf(b);
if (indexA === -1) return 1;
if (indexB === -1) return -1;
return indexA - indexB;
});
const firstCategory = sortedCategories[0];
const hasMore = sortedCategories.length > 1;
if (!hasMore) {
return (
<div className={className}>
<Badge category={firstCategory} />
</div>
);
}
return (
<HoverCard open={open} onOpenChange={setOpen}>
<HoverCardTrigger asChild>
<button
type="button"
className={cn("cursor-pointer bg-transparent p-0 border-0", className)}
aria-label={`View ${categories.length} retention categories`}
onClick={() => setOpen(true)}
>
<Badge category={`${categories.length} tags`} />
</button>
</HoverCardTrigger>
<HoverCardContent className="w-auto p-2">
<div className="flex flex-wrap gap-1">
{sortedCategories.map((category) => (
<Badge key={category} category={category} />
))}
</div>
</HoverCardContent>
</HoverCard>
);
}

View file

@ -1,12 +1,11 @@
import { useState } from "react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { Calendar, Clock, Database, FolderTree, HardDrive, Trash2 } from "lucide-react";
import { useNavigate } from "react-router";
import { Calendar, Clock, Database, HardDrive, Tag, Trash2, X } from "lucide-react";
import { toast } from "sonner";
import { ByteSize } from "~/client/components/bytes-size";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/client/components/ui/table";
import { Tooltip, TooltipContent, TooltipTrigger } from "~/client/components/ui/tooltip";
import { Button } from "~/client/components/ui/button";
import { Checkbox } from "~/client/components/ui/checkbox";
import {
AlertDialog,
AlertDialogAction,
@ -17,163 +16,309 @@ import {
AlertDialogHeader,
AlertDialogTitle,
} from "~/client/components/ui/alert-dialog";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "~/client/components/ui/dialog";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/client/components/ui/select";
import { useTimeFormat } from "~/client/lib/datetime";
import { formatDuration } from "~/utils/utils";
import type { ListSnapshotsResponse } from "../api-client";
import { deleteSnapshotMutation } from "~/client/api-client/@tanstack/react-query.gen";
import {
deleteSnapshotsMutation,
listSnapshotsQueryKey,
tagSnapshotsMutation,
} from "~/client/api-client/@tanstack/react-query.gen";
import { parseError } from "~/client/lib/errors";
type Snapshot = ListSnapshotsResponse[number];
import type { BackupSchedule, Snapshot } from "../lib/types";
import { cn } from "../lib/utils";
import { Link, useNavigate } from "@tanstack/react-router";
import type { ListSnapshotsData } from "~/client/api-client/types.gen";
import type { Options } from "~/client/api-client/client/types.gen";
type Props = {
snapshots: Snapshot[];
repositoryName: string;
backups: BackupSchedule[];
repositoryId: string;
listSnapshotsQueryOptions: Options<ListSnapshotsData>;
};
export const SnapshotsTable = ({ snapshots, repositoryName }: Props) => {
const navigate = useNavigate();
export const SnapshotsTable = ({ snapshots, repositoryId, backups, listSnapshotsQueryOptions }: Props) => {
const queryClient = useQueryClient();
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [snapshotToDelete, setSnapshotToDelete] = useState<string | null>(null);
const navigate = useNavigate();
const { formatDateTime } = useTimeFormat();
const deleteSnapshot = useMutation({
...deleteSnapshotMutation(),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["listSnapshots"] });
setShowDeleteConfirm(false);
setSnapshotToDelete(null);
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
const [showBulkDeleteConfirm, setShowBulkDeleteConfirm] = useState(false);
const [showReTagDialog, setShowReTagDialog] = useState(false);
const [targetScheduleId, setTargetScheduleId] = useState<string>("");
const deleteSnapshots = useMutation({
...deleteSnapshotsMutation(),
onSuccess: (_data, variables) => {
const snapshotIds = variables.body?.snapshotIds ?? [];
const queryKey = listSnapshotsQueryKey(listSnapshotsQueryOptions);
queryClient.setQueryData<Snapshot[]>(queryKey, (old) => {
if (!old) return old;
return old.filter((snapshot) => !snapshotIds.includes(snapshot.short_id));
});
void queryClient.invalidateQueries({ queryKey });
setShowBulkDeleteConfirm(false);
setSelectedIds(new Set());
},
});
const handleDeleteClick = (e: React.MouseEvent, snapshotId: string) => {
e.stopPropagation();
setSnapshotToDelete(snapshotId);
setShowDeleteConfirm(true);
const tagSnapshots = useMutation({
...tagSnapshotsMutation(),
onMutate: () => {
setShowReTagDialog(false);
},
onSuccess: () => {
setShowReTagDialog(false);
setSelectedIds(new Set());
setTargetScheduleId("");
},
});
const handleRowClick = (snapshotId: string) => {
void navigate({ to: `/repositories/${repositoryId}/${snapshotId}` });
};
const handleConfirmDelete = () => {
if (snapshotToDelete) {
toast.promise(
deleteSnapshot.mutateAsync({
path: { name: repositoryName, snapshotId: snapshotToDelete },
}),
{
loading: "Deleting snapshot...",
success: "Snapshot deleted successfully",
error: (error) => parseError(error)?.message || "Failed to delete snapshot",
},
);
const toggleSelectAll = () => {
if (selectedIds.size === snapshots.length) {
setSelectedIds(new Set());
} else {
setSelectedIds(new Set(snapshots.map((s) => s.short_id)));
}
};
const handleRowClick = (snapshotId: string) => {
navigate(`/repositories/${repositoryName}/${snapshotId}`);
const handleBulkDelete = () => {
toast.promise(
deleteSnapshots.mutateAsync({
path: { shortId: repositoryId },
body: { snapshotIds: Array.from(selectedIds) },
}),
{
loading: `Deleting ${selectedIds.size} snapshots...`,
success: "Snapshots deleted successfully",
error: (error) => parseError(error)?.message || "Failed to delete snapshots",
},
);
};
const handleBulkReTag = () => {
const schedule = backups.find((b) => b.shortId === targetScheduleId);
if (!schedule) return;
toast.promise(
tagSnapshots.mutateAsync({
path: { shortId: repositoryId },
body: {
snapshotIds: Array.from(selectedIds),
set: [schedule.shortId],
},
}),
{
loading: `Re-tagging ${selectedIds.size} snapshots...`,
success: `Snapshots re-tagged to ${schedule.name}`,
error: (error) => parseError(error)?.message || "Failed to re-tag snapshots",
},
);
};
return (
<>
<div className="overflow-x-auto">
<div className="overflow-x-auto relative">
<Table className="border-t">
<TableHeader className="bg-card-header">
<TableRow>
<TableHead className="w-10">
<Checkbox
checked={selectedIds.size === snapshots.length && snapshots.length > 0}
onCheckedChange={toggleSelectAll}
aria-label="Select all"
/>
</TableHead>
<TableHead className="uppercase">Snapshot ID</TableHead>
<TableHead className="uppercase">Schedule</TableHead>
<TableHead className="uppercase">Date & Time</TableHead>
<TableHead className="uppercase">Size</TableHead>
<TableHead className="uppercase hidden md:table-cell text-right">Duration</TableHead>
<TableHead className="uppercase hidden text-right lg:table-cell">Paths</TableHead>
<TableHead className="uppercase text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{snapshots.map((snapshot) => (
<TableRow
key={snapshot.short_id}
className="hover:bg-accent/50 cursor-pointer"
onClick={() => handleRowClick(snapshot.short_id)}
>
<TableCell className="font-mono text-sm">
<div className="flex items-center gap-2">
<HardDrive className="h-4 w-4 text-muted-foreground" />
<span className="text-strong-accent">{snapshot.short_id}</span>
</div>
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<Calendar className="h-4 w-4 text-muted-foreground" />
<span className="text-sm">{new Date(snapshot.time).toLocaleString()}</span>
</div>
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<Database className="h-4 w-4 text-muted-foreground" />
<span className="font-medium">
<ByteSize bytes={snapshot.size} base={1024} />
</span>
</div>
</TableCell>
<TableCell className="hidden md:table-cell">
<div className="flex items-center justify-end gap-2">
<Clock className="h-4 w-4 text-muted-foreground" />
<span className="text-sm text-muted-foreground">{formatDuration(snapshot.duration / 1000)}</span>
</div>
</TableCell>
<TableCell className="hidden lg:table-cell">
<div className="flex items-center justify-end gap-2">
<FolderTree className="h-4 w-4 text-muted-foreground" />
<Tooltip>
<TooltipTrigger asChild>
<span className="text-xs bg-primary/10 text-primary rounded-md px-2 py-1 cursor-help">
{snapshot.paths.length} {snapshot.paths.length === 1 ? "path" : "paths"}
</span>
</TooltipTrigger>
<TooltipContent side="top" className="max-w-md">
<div className="flex flex-col gap-1">
{snapshot.paths.map((path) => (
<div key={`${snapshot.short_id}-${path}`} className="text-xs font-mono">
{path}
</div>
))}
</div>
</TooltipContent>
</Tooltip>
</div>
</TableCell>
<TableCell className="text-right">
<Button
variant="ghost"
size="sm"
onClick={(e) => handleDeleteClick(e, snapshot.short_id)}
disabled={deleteSnapshot.isPending}
>
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
</TableCell>
</TableRow>
))}
{snapshots.map((snapshot) => {
const backup = backups.find((b) => snapshot.tags.includes(b.shortId));
const isSelected = selectedIds.has(snapshot.short_id);
return (
<TableRow
key={snapshot.short_id}
className={cn("hover:bg-accent/50 cursor-pointer", isSelected && "bg-accent/30")}
onClick={() => handleRowClick(snapshot.short_id)}
>
<TableCell onClick={(e) => e.stopPropagation()}>
<Checkbox
checked={isSelected}
onCheckedChange={() => {
const newSelected = new Set(selectedIds);
if (newSelected.has(snapshot.short_id)) {
newSelected.delete(snapshot.short_id);
} else {
newSelected.add(snapshot.short_id);
}
setSelectedIds(newSelected);
}}
aria-label={`Select snapshot ${snapshot.short_id}` as string}
/>
</TableCell>
<TableCell className="font-mono text-sm">
<div className="flex items-center gap-2">
<HardDrive className="h-4 w-4 text-muted-foreground" />
<span className="text-strong-accent">{snapshot.short_id}</span>
</div>
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<Link
hidden={!backup}
to={backup ? `/backups/$backupId` : "."}
params={backup ? { backupId: backup.shortId } : {}}
onClick={(e) => e.stopPropagation()}
className="hover:underline"
>
<span className="text-sm">{backup ? backup.name : "-"}</span>
</Link>
<span hidden={!!backup} className="text-sm text-muted-foreground">
-
</span>
</div>
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<Calendar className="h-4 w-4 text-muted-foreground" />
<span className="text-sm">{formatDateTime(snapshot.time)}</span>
</div>
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<Database className="h-4 w-4 text-muted-foreground" />
<span className="font-medium">
<ByteSize bytes={snapshot.size} base={1024} />
</span>
</div>
</TableCell>
<TableCell className="hidden md:table-cell">
<div className="flex items-center justify-end gap-2">
<Clock className="h-4 w-4 text-muted-foreground" />
<span className="text-sm text-muted-foreground">{formatDuration(snapshot.duration / 1000)}</span>
</div>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>
<AlertDialog open={showDeleteConfirm} onOpenChange={setShowDeleteConfirm}>
{selectedIds.size > 0 && (
<div className="fixed bottom-6 left-1/2 -translate-x-1/2 z-50 animate-in fade-in slide-in-from-bottom-4 duration-300">
<div className="bg-card border shadow-2xl rounded-full px-4 py-2 flex items-center gap-4 min-w-75 justify-between">
<div className="flex items-center gap-3 border-r pr-4">
<Button
variant="ghost"
size="icon"
className="h-8 w-8 rounded-full"
onClick={() => setSelectedIds(new Set())}
>
<X className="h-4 w-4" />
</Button>
<span className="text-sm font-medium">{selectedIds.size} selected</span>
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
className="rounded-full gap-2"
onClick={() => setShowReTagDialog(true)}
>
<Tag className="h-4 w-4 mr-2" />
Re-tag
</Button>
<Button
variant="destructive"
size="sm"
className="rounded-full gap-2"
onClick={() => setShowBulkDeleteConfirm(true)}
>
<Trash2 className="h-4 w-4 mr-2" />
Delete
</Button>
</div>
</div>
</div>
)}
<AlertDialog open={showBulkDeleteConfirm} onOpenChange={setShowBulkDeleteConfirm}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete snapshot?</AlertDialogTitle>
<AlertDialogTitle>Delete {selectedIds.size} snapshots?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This will permanently delete the snapshot and all its data from the
repository.
This action cannot be undone. This will permanently delete the selected snapshots and all their data from
the repository.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={handleConfirmDelete}
disabled={deleteSnapshot.isPending}
onClick={handleBulkDelete}
disabled={deleteSnapshots.isPending}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
Delete snapshot
Delete {selectedIds.size} snapshots
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<Dialog open={showReTagDialog} onOpenChange={setShowReTagDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle>Re-tag snapshots</DialogTitle>
<DialogDescription>
Select a backup schedule to re-tag the {selectedIds.size} selected snapshots. All {selectedIds.size}{" "}
selected snapshots will be associated with the chosen schedule.
</DialogDescription>
</DialogHeader>
<div className="py-4">
<Select value={targetScheduleId} onValueChange={setTargetScheduleId}>
<SelectTrigger>
<SelectValue placeholder="Select a schedule" />
</SelectTrigger>
<SelectContent>
{backups.map((backup) => (
<SelectItem key={backup.shortId} value={backup.shortId}>
{backup.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setShowReTagDialog(false)}>
Cancel
</Button>
<Button onClick={handleBulkReTag} disabled={!targetScheduleId}>
Apply tags
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
};

View file

@ -0,0 +1,37 @@
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { ClientOnly } from "@tanstack/react-router";
import { GripVertical } from "lucide-react";
import type { PropsWithChildren } from "react";
interface SortableBackupCardProps {
isDragging?: boolean;
uniqueId: string | number;
}
export function SortableCard({ isDragging, uniqueId, children }: PropsWithChildren<SortableBackupCardProps>) {
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({
id: uniqueId,
});
const style = {
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.5 : 1,
};
return (
<div ref={setNodeRef} style={style} className="relative group">
<ClientOnly>
<div
{...attributes}
{...listeners}
className="absolute left-1/2 -translate-x-1/2 top-1 z-10 cursor-grab active:cursor-grabbing opacity-100 md:opacity-0 group-hover:opacity-100 transition-opacity p-1 rounded hover:bg-muted/50 bg-background/80 backdrop-blur-sm"
>
<GripVertical className="h-4 w-4 text-muted-foreground rotate-90" />
</div>
</ClientOnly>
{children}
</div>
);
}

View file

@ -1,48 +1,60 @@
import type { VolumeStatus } from "~/client/lib/types";
import { cn } from "~/client/lib/utils";
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
export const StatusDot = ({ status }: { status: VolumeStatus }) => {
type StatusVariant = "success" | "neutral" | "error" | "warning" | "info";
interface StatusDotProps {
variant: StatusVariant;
label: string;
animated?: boolean;
}
export const StatusDot = ({ variant, label, animated }: StatusDotProps) => {
const statusMapping = {
mounted: {
color: "bg-green-500",
colorLight: "bg-emerald-400",
animated: true,
success: {
color: "bg-success shadow-[0_0_8px_rgba(16,185,129,0.5)]",
colorLight: "bg-success/60",
animated: animated ?? true,
},
unmounted: {
neutral: {
color: "bg-gray-500",
colorLight: "bg-gray-400",
animated: false,
animated: animated ?? false,
},
error: {
color: "bg-red-500",
colorLight: "bg-amber-700",
animated: true,
colorLight: "bg-red-400",
animated: animated ?? true,
},
unknown: {
warning: {
color: "bg-yellow-500",
colorLight: "bg-yellow-400",
animated: true,
animated: animated ?? true,
},
}[status];
info: {
color: "bg-blue-500",
colorLight: "bg-blue-400",
animated: animated ?? true,
},
}[variant];
return (
<Tooltip>
<TooltipTrigger>
<TooltipTrigger asChild aria-label={label}>
<span className="relative flex size-3 mx-auto">
{statusMapping.animated && (
{statusMapping?.animated && (
<span
className={cn(
"absolute inline-flex h-full w-full animate-ping rounded-full opacity-75",
"absolute inline-flex h-full w-full animate-[ping_3s_cubic-bezier(0,0,0.2,1)_infinite] rounded-full opacity-50",
`${statusMapping.colorLight}`,
)}
/>
)}
<span className={cn("relative inline-flex size-3 rounded-full", `${statusMapping.color}`)} />
<span className={cn("relative inline-flex size-3 rounded-full", `${statusMapping?.color}`)} />
</span>
</TooltipTrigger>
<TooltipContent>
<p className="capitalize">{status}</p>
<p className="capitalize">{label}</p>
</TooltipContent>
</Tooltip>
);

View file

@ -0,0 +1,35 @@
import { createContext, useCallback, useContext, useState } from "react";
type Theme = "light" | "dark";
type ThemeContextValue = {
theme: Theme;
setTheme: (theme: Theme) => void;
};
const ThemeContext = createContext<ThemeContextValue | null>(null);
export const THEME_COOKIE_NAME = "theme";
const THEME_COOKIE_MAX_AGE = 60 * 60 * 24 * 365; // 1 year
const DEFAULT_THEME: Theme = "dark";
export function ThemeProvider({ children, initialTheme }: { children: React.ReactNode; initialTheme?: Theme }) {
const [theme, setThemeState] = useState<Theme>(initialTheme ?? DEFAULT_THEME);
const setTheme = useCallback((newTheme: Theme) => {
setThemeState(newTheme);
document.cookie = `${THEME_COOKIE_NAME}=${newTheme}; path=/; max-age=${THEME_COOKIE_MAX_AGE}`;
document.documentElement.classList.toggle("dark", newTheme === "dark");
document.documentElement.style.colorScheme = newTheme;
}, []);
return <ThemeContext value={{ theme, setTheme }}>{children}</ThemeContext>;
}
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error("useTheme must be used within a ThemeProvider");
}
return context;
}

View file

@ -0,0 +1,26 @@
import { Moon, Sun } from "lucide-react";
import { useTheme } from "./theme-provider";
import { Button } from "./ui/button";
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
export function ThemeToggle() {
const { theme, setTheme } = useTheme();
return (
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="relative rounded-full h-7 w-7 text-muted-foreground hover:text-foreground"
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
aria-label="Toggle theme"
>
<Sun className="h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
</Button>
</TooltipTrigger>
<TooltipContent>Toggle theme</TooltipContent>
</Tooltip>
);
}

View file

@ -0,0 +1,12 @@
import { useTimeFormat, type DateInput } from "~/client/lib/datetime";
type Props = {
date: DateInput;
className?: string;
};
export function TimeAgo({ date, className }: Props) {
const { formatTimeAgo } = useTimeFormat();
return <span className={className}>{formatTimeAgo(date)}</span>;
}

View file

@ -1,3 +1,4 @@
// fallow-ignore-file unused-export
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
import type * as React from "react";
import { buttonVariants } from "~/client/components/ui/button";

View file

@ -27,9 +27,9 @@ const Alert = React.forwardRef<
));
Alert.displayName = "Alert";
const AlertTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
const AlertTitle = React.forwardRef<HTMLHeadingElement, React.HTMLAttributes<HTMLHeadingElement>>(
({ className, ...props }, ref) => (
<h5 ref={ref} className={cn("mb-1 font-medium leading-none tracking-tight", className)} {...props} />
<h5 ref={ref} className={cn("mb-1 font-medium leading-none tracking-tight", className)} {...props}>{props.children}</h5>
),
);
AlertTitle.displayName = "AlertTitle";

View file

@ -1,3 +1,4 @@
// fallow-ignore-file unused-export
import type * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";

View file

@ -1,28 +1,28 @@
// fallow-ignore-file unused-export
import type * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { ChevronRight, MoreHorizontal } from "lucide-react";
import { cn } from "~/client/lib/utils";
function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />;
function Breadcrumb({ className, ...props }: React.ComponentProps<"nav">) {
return <nav aria-label="breadcrumb" data-slot="breadcrumb" className={cn("min-w-0", className)} {...props} />;
}
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
return (
<ol
data-slot="breadcrumb-list"
className={cn(
"text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm wrap-break-words sm:gap-2.5",
className,
)}
className={cn("text-muted-foreground flex items-center gap-1.5 text-sm sm:gap-2.5 min-w-0", className)}
{...props}
/>
);
}
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
return <li data-slot="breadcrumb-item" className={cn("inline-flex items-center gap-1.5", className)} {...props} />;
return (
<li data-slot="breadcrumb-item" className={cn("inline-flex items-center gap-1.5 min-w-0", className)} {...props} />
);
}
function BreadcrumbLink({
@ -35,20 +35,25 @@ function BreadcrumbLink({
const Comp = asChild ? Slot : "a";
return (
<Comp data-slot="breadcrumb-link" className={cn("hover:text-foreground transition-colors", className)} {...props} />
<Comp
data-slot="breadcrumb-link"
className={cn("hover:text-foreground transition-colors truncate", className)}
{...props}
/>
);
}
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
function BreadcrumbPage({ className, children, ...props }: React.ComponentProps<"a">) {
return (
<span
<a
data-slot="breadcrumb-page"
role="link"
aria-disabled="true"
aria-current="page"
className={cn("text-foreground font-normal", className)}
className={cn("text-foreground font-normal truncate", className)}
{...props}
/>
>
{children}
</a>
);
}

View file

@ -1,21 +1,22 @@
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { Loader2 } from "lucide-react";
import type * as React from "react";
import * as React from "react";
import { cn } from "~/client/lib/utils";
import { useMinimumDuration } from "~/client/hooks/useMinimumDuration";
const buttonVariants = cva(
"inline-flex cursor-pointer uppercase rounded-sm items-center justify-center gap-2 whitespace-nowrap text-xs font-semibold tracking-wide transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-ring border-0",
"inline-flex cursor-pointer uppercase rounded-sm items-center justify-center gap-2 whitespace-nowrap text-xs font-semibold tracking-wide transition-all active:scale-[0.98] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-ring border-0",
{
variants: {
variant: {
default: "bg-transparent text-white hover:bg-[#3A3A3A]/80 border 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",
},
@ -33,28 +34,32 @@ const buttonVariants = cva(
},
);
const MINIMUM_LOADING_DURATION = 300;
function Button({
className,
variant,
size,
asChild = false,
loading,
disabled,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean;
} & { loading?: boolean }) {
const Comp = asChild ? Slot : "button";
const isLoading = useMinimumDuration(loading ?? false, MINIMUM_LOADING_DURATION);
return (
<Comp
disabled={loading}
{...props}
data-slot="button"
className={cn(buttonVariants({ variant, size, className }), "transition-all")}
{...props}
disabled={disabled || loading || isLoading}
>
<Loader2 className={cn("h-4 w-4 animate-spin absolute", { invisible: !loading })} />
<div className={cn("flex items-center justify-center", { invisible: loading })}>{props.children}</div>
<Loader2 className={cn("h-4 w-4 animate-spin absolute", { invisible: !isLoading })} />
<div className={cn("flex items-center justify-center", { invisible: isLoading })}>{props.children}</div>
</Comp>
);
}

View file

@ -1,23 +1,35 @@
// fallow-ignore-file unused-export
import type * as React from "react";
import { cn } from "~/client/lib/utils";
function Card({ className, children, ...props }: React.ComponentProps<"div">) {
function Card({ className, children, interactive, ...props }: React.ComponentProps<"div"> & { interactive?: boolean }) {
return (
<div
data-slot="card"
className={cn("bg-card text-card-foreground relative flex flex-col gap-6 border-2 py-6 shadow-sm", className)}
className={cn(
"bg-card text-card-foreground group relative flex flex-col gap-6 border border-border py-6 shadow-[0_8px_30px_-15px_rgba(0,0,0,0.04)] dark:shadow-[inset_0_0_0_1px_rgba(255,255,255,0.02)] transition-colors duration-300 ",
className,
)}
{...props}
>
<span aria-hidden="true" className="pointer-events-none absolute inset-0 z-10 select-none">
<span className="absolute left-[-2px] top-[-2px] h-0.5 w-4 bg-white/80" />
<span className="absolute left-[-2px] top-[-2px] h-4 w-0.5 bg-white/80" />
<span className="absolute right-[-2px] top-[-2px] h-0.5 w-4 bg-white/80" />
<span className="absolute right-[-2px] top-[-2px] h-4 w-0.5 bg-white/80" />
<span className="absolute left-[-2px] bottom-[-2px] h-0.5 w-4 bg-white/80" />
<span className="absolute left-[-2px] bottom-[-2px] h-4 w-0.5 bg-white/80" />
<span className="absolute right-[-2px] bottom-[-2px] h-0.5 w-4 bg-white/80" />
<span className="absolute right-[-2px] bottom-[-2px] h-4 w-0.5 bg-white/80" />
<span
aria-hidden="true"
className={cn(
"pointer-events-none absolute inset-0 z-10 select-none opacity-30 transition-opacity duration-300 ",
{
"group-hover:opacity-100": interactive,
},
)}
>
<span className="absolute -left-0.5 -top-0.5 h-0.5 w-4 bg-foreground" />
<span className="absolute -left-0.5 -top-0.5 h-4 w-0.5 bg-foreground" />
<span className="absolute -right-0.5 -top-0.5 h-0.5 w-4 bg-foreground" />
<span className="absolute -right-0.5 -top-0.5 h-4 w-0.5 bg-foreground" />
<span className="absolute -left-0.5 -bottom-0.5 h-0.5 w-4 bg-foreground" />
<span className="absolute -left-0.5 -bottom-0.5 h-4 w-0.5 bg-foreground" />
<span className="absolute -right-0.5 -bottom-0.5 h-0.5 w-4 bg-foreground" />
<span className="absolute -right-0.5 -bottom-0.5 h-4 w-0.5 bg-foreground" />
</span>
{children}
</div>

View file

@ -1,13 +1,16 @@
// fallow-ignore-file unused-export
// @ts-nocheck
// biome-ignore-all lint: reason
import * as React from "react";
import * as RechartsPrimitive from "recharts";
import { useRootLoaderData } from "~/client/hooks/use-root-loader-data";
import { cn } from "~/client/lib/utils";
// Format: { THEME_NAME: CSS_SELECTOR }
const THEMES = { light: "", dark: ".dark" } as const;
// fallow-ignore-next-line unused-type
export type ChartConfig = {
[k in string]: {
label?: React.ReactNode;
@ -116,6 +119,7 @@ function ChartTooltipContent({
labelKey?: string;
}) {
const { config } = useChart();
const { locale } = useRootLoaderData();
const tooltipLabel = React.useMemo(() => {
if (hideLabel || !payload?.length) {
@ -205,7 +209,7 @@ function ChartTooltipContent({
</div>
{item.value && (
<span className="text-foreground font-mono font-medium tabular-nums">
{item.value.toLocaleString()}
{item.value.toLocaleString(locale)}
</span>
)}
</div>

View file

@ -1,10 +1,13 @@
import type * as React from "react";
import * as React from "react";
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
import { CheckIcon } from "lucide-react";
import { useWebHaptics } from "web-haptics/react";
import { cn } from "~/client/lib/utils";
function Checkbox({ className, ...props }: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
const trigger = useWebHaptics().trigger;
return (
<CheckboxPrimitive.Root
data-slot="checkbox"
@ -12,6 +15,10 @@ function Checkbox({ className, ...props }: React.ComponentProps<typeof CheckboxP
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
onCheckedChange={(checked) => {
void trigger([{ duration: 8 }], { intensity: 0.3 });
props.onCheckedChange?.(checked);
}}
{...props}
>
<CheckboxPrimitive.Indicator

View file

@ -1,6 +1,4 @@
import type React from "react";
import { toast } from "sonner";
import { copyToClipboard } from "~/utils/clipboard";
interface CodeBlockProps {
code: string;
@ -9,30 +7,18 @@ interface CodeBlockProps {
}
export const CodeBlock: React.FC<CodeBlockProps> = ({ code, filename }) => {
const handleCopy = async () => {
await copyToClipboard(code);
toast.success("Code copied to clipboard");
};
return (
<div className="overflow-hidden rounded-sm bg-card-header ring-1 ring-white/10">
<div className="flex items-center justify-between border-b border-white/10 px-4 py-2 text-xs">
<div className="overflow-hidden rounded-sm bg-card-header ring-1 ring-border">
<div className="flex items-center justify-between border-b border-border px-4 py-2 text-xs">
<div className="flex items-center gap-1.5">
<span className="h-2.5 w-2.5 rounded-full bg-rose-500" />
<span className="h-2.5 w-2.5 rounded-full bg-amber-500" />
<span className="h-2.5 w-2.5 rounded-full bg-emerald-500" />
{filename && <span className="ml-3 font-medium">{filename}</span>}
</div>
<button
type="button"
onClick={() => handleCopy()}
className="cursor-pointer rounded-md bg-white/5 px-2 py-1 text-[11px] font-medium ring-1 ring-inset ring-white/10 transition hover:bg-white/10 active:translate-y-px"
>
Copy
</button>
</div>
<pre className="text-xs m-0 px-4 py-2 bg-card-header">
<code className="text-white/80">{code}</code>
<code className="text-foreground/80 select-all">{code}</code>
</pre>
</div>
);

View file

@ -0,0 +1,98 @@
import * as React from "react";
import { ChevronDown } from "lucide-react";
import { cn } from "~/client/lib/utils";
interface CollapsibleProps extends React.HTMLAttributes<HTMLDivElement> {
open?: boolean;
onOpenChange?: (open: boolean) => void;
defaultOpen?: boolean;
}
const CollapsibleContext = React.createContext<{
open: boolean;
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
}>({
open: false,
setOpen: () => {},
});
const Collapsible = React.forwardRef<HTMLDivElement, CollapsibleProps>(
({ className, open: controlledOpen, onOpenChange, defaultOpen = false, children, ...props }, ref) => {
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen);
const isControlled = controlledOpen !== undefined;
const open = isControlled ? controlledOpen : uncontrolledOpen;
const setOpen = React.useCallback(
(value: React.SetStateAction<boolean>) => {
const newValue = typeof value === "function" ? value(open) : value;
if (!isControlled) {
setUncontrolledOpen(newValue);
}
onOpenChange?.(newValue);
},
[isControlled, open, onOpenChange],
);
return (
<CollapsibleContext.Provider value={{ open, setOpen }}>
<div ref={ref} className={cn(className)} {...props}>
{children}
</div>
</CollapsibleContext.Provider>
);
},
);
Collapsible.displayName = "Collapsible";
interface CollapsibleTriggerProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {}
const CollapsibleTrigger = React.forwardRef<HTMLButtonElement, CollapsibleTriggerProps>(
({ className, children, ...props }, ref) => {
const { open, setOpen } = React.useContext(CollapsibleContext);
return (
<button
ref={ref}
type="button"
className={cn(
"flex gap-2 items-center justify-between py-2 text-sm font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
className,
)}
data-state={open ? "open" : "closed"}
onClick={() => setOpen(!open)}
{...props}
>
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
{children}
</button>
);
},
);
CollapsibleTrigger.displayName = "CollapsibleTrigger";
interface CollapsibleContentProps extends React.HTMLAttributes<HTMLDivElement> {}
const CollapsibleContent = React.forwardRef<HTMLDivElement, CollapsibleContentProps>(
({ className, children, ...props }, ref) => {
const { open } = React.useContext(CollapsibleContext);
return (
<div
ref={ref}
className={cn(
"overflow-hidden transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down",
className,
)}
data-state={open ? "open" : "closed"}
hidden={!open}
{...props}
>
{open && children}
</div>
);
},
);
CollapsibleContent.displayName = "CollapsibleContent";
export { Collapsible, CollapsibleTrigger, CollapsibleContent };

View file

@ -1,3 +1,4 @@
// fallow-ignore-file unused-export
import type * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { XIcon } from "lucide-react";

View file

@ -0,0 +1,218 @@
// fallow-ignore-file unused-export
import * as React from "react";
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { cn } from "~/client/lib/utils";
function DropdownMenu({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
}
function DropdownMenuPortal({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
return <DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />;
}
function DropdownMenuTrigger({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
return <DropdownMenuPrimitive.Trigger data-slot="dropdown-menu-trigger" {...props} />;
}
function DropdownMenuContent({
className,
sideOffset = 4,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
className,
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
);
}
function DropdownMenuGroup({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
return <DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />;
}
function DropdownMenuItem({
className,
inset,
variant = "default",
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean;
variant?: "default" | "destructive";
}) {
return (
<DropdownMenuPrimitive.Item
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
/>
);
}
function DropdownMenuCheckboxItem({
className,
children,
checked,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
return (
<DropdownMenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
);
}
function DropdownMenuRadioGroup({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
return <DropdownMenuPrimitive.RadioGroup data-slot="dropdown-menu-radio-group" {...props} />;
}
function DropdownMenuRadioItem({
className,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
return (
<DropdownMenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
);
}
function DropdownMenuLabel({
className,
inset,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean;
}) {
return (
<DropdownMenuPrimitive.Label
data-slot="dropdown-menu-label"
data-inset={inset}
className={cn("px-2 py-1.5 text-sm font-medium data-[inset]:pl-8", className)}
{...props}
/>
);
}
function DropdownMenuSeparator({ className, ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
return (
<DropdownMenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
);
}
function DropdownMenuShortcut({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
data-slot="dropdown-menu-shortcut"
className={cn("text-muted-foreground ml-auto text-xs tracking-widest", className)}
{...props}
/>
);
}
function DropdownMenuSub({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
}
function DropdownMenuSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean;
}) {
return (
<DropdownMenuPrimitive.SubTrigger
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger>
);
}
function DropdownMenuSubContent({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
return (
<DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className,
)}
{...props}
/>
);
}
export {
DropdownMenu,
DropdownMenuPortal,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
};

Some files were not shown because too many files have changed in this diff Show more