Compare commits

..

399 commits

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
729 changed files with 90002 additions and 14891 deletions

View file

@ -1,21 +1,19 @@
**
!turbo.json
!bun.lock
!package.json
!server.ts
!.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
@ -26,3 +24,4 @@
node_modules/**
dist/**
.output/**
app/test/integration/artifacts/**

View file

@ -4,5 +4,6 @@ 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:300
BASE_URL=http://*.localhost
TRUST_PROXY=false
PORT=3000

View file

@ -1,3 +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"]
}

View file

@ -1,15 +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: oven-sh/setup-bun@v2
- uses: socketdev/action@937f824ec476dfd164d4a4d9995751427b0be143 # v1
with:
mode: firewall
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
name: Install Bun
with:
bun-version: "1.3.6"
bun-version: "1.3.14"
- uses: voidzero-dev/setup-vp@ca1c46663915d6c1042ae23bd39ab85718bfb0fa # v1
with:
node-version: '24'
cache: true
- name: Install dependencies
shell: bash
run: bun install --frozen-lockfile
run: |
cd ${{ inputs.workingDirectory }}
sfw vp install --frozen-lockfile

View file

@ -1,18 +0,0 @@
version: 2
updates:
- package-ecosystem: "bun"
directory: "/"
schedule:
interval: "daily"
rebase-strategy: 'auto'
groups:
minor-patch:
update-types:
- minor
- patch
- package-ecosystem: 'github-actions'
directory: '/'
schedule:
interval: 'daily'
rebase-strategy: 'auto'

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"
}
]
}

View file

@ -3,8 +3,6 @@ name: Checks
on:
workflow_call:
pull_request:
branches:
- main
push:
branches:
- main
@ -12,31 +10,86 @@ on:
permissions:
contents: read
concurrency:
group: checks-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
checks:
timeout-minutes: 15
lint:
timeout-minutes: 10
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
fetch-depth: 1
- name: Install dependencies
uses: "./.github/actions/install-dependencies"
- uses: oxc-project/oxlint-action@latest
- 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:
config: .oxlintrc.json
deny-warnings: true
fetch-depth: 1
- name: Install dependencies
uses: "./.github/actions/install-dependencies"
- name: Run type checks
shell: bash
run: bun run tsc
- name: Run tests
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 --ci --coverage
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

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

View file

@ -12,7 +12,7 @@ jobs:
timeout-minutes: 20
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Install dependencies
uses: "./.github/actions/install-dependencies"
@ -26,7 +26,7 @@ jobs:
- name: Cache Playwright Browsers
id: playwright-cache
uses: actions/cache@v5
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: ~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ steps.playwright-version.outputs.version }}
@ -46,22 +46,37 @@ jobs:
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 logs zerobyte || true
run: docker compose logs zerobyte-e2e tinyauth tinyauth-app || true
- name: Make playwright directory writable
run: sudo chmod 777 playwright/data/zerobyte.db
@ -72,15 +87,16 @@ jobs:
- name: Dump container logs in playwright-report folder
if: always()
run: |
tree playwright
docker logs zerobyte > playwright-report/container-logs.txt || true
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@v6
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
if: always()
with:
name: playwright-report

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

View file

@ -6,31 +6,34 @@ on:
workflow_dispatch:
permissions:
packages: write
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@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Log in to Docker Hub
uses: docker/login-action@v3
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 }}
@ -38,14 +41,14 @@ jobs:
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
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@v6
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7
with:
context: .
target: production

View file

@ -8,16 +8,16 @@ on:
- "v*.*.*-alpha.*"
permissions:
contents: write
packages: write
security-events: write
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
@ -31,74 +31,60 @@ 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, checks, e2e-tests]
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@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
ref: ${{ github.ref }}
- name: Log in to Docker Hub
uses: docker/login-action@v3
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 }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build docker image
uses: docker/build-push-action@v6
with:
context: .
target: production
platforms: linux/amd64
push: false
load: true
tags: local/zerobyte:ci
build-args: |
APP_VERSION=${{ needs.determine-release-type.outputs.tagname }}
- name: Scan new image for vulnerabilities
if: needs.determine-release-type.outputs.release_type == 'release'
uses: anchore/scan-action@v7
id: scan
with:
image: local/zerobyte:ci
fail-build: false
only-fixed: true
severity-cutoff: critical
- name: upload Anchore scan report
if: needs.determine-release-type.outputs.release_type == 'release'
uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: ${{ steps.scan.outputs.sarif }}
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6
with:
images: ghcr.io/${{ github.repository_owner }}/zerobyte
tags: |
@ -110,7 +96,7 @@ jobs:
latest=${{ needs.determine-release-type.outputs.release_type == 'release' }}
- name: Push images to GitHub Container Registry
uses: docker/build-push-action@v6
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7
with:
context: .
target: production
@ -125,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:
@ -140,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

14
.gitignore vendored
View file

@ -15,7 +15,7 @@ notes.md
smb-password.txt
cache.db
data/
/data
.env*
!.env.example
@ -28,13 +28,25 @@ node_modules/
/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

View file

@ -1,7 +0,0 @@
{
"$schema": "./node_modules/oxfmt/configuration_schema.json",
"printWidth": 120,
"useTabs": true,
"endOfLine": "lf",
"ignorePatterns": ["*.gen.ts"]
}

View file

@ -1,152 +0,0 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["eslint", "unicorn", "typescript", "oxc", "import", "react", "react-perf", "node", "jsx-a11y"],
"categories": {},
"rules": {
"constructor-super": "warn",
"for-direction": "warn",
"no-async-promise-executor": "warn",
"no-caller": "warn",
"no-class-assign": "warn",
"no-compare-neg-zero": "warn",
"no-cond-assign": "warn",
"no-const-assign": "warn",
"no-constant-binary-expression": "warn",
"no-constant-condition": "warn",
"no-control-regex": "warn",
"no-debugger": "warn",
"no-delete-var": "warn",
"no-dupe-class-members": "warn",
"no-dupe-else-if": "warn",
"no-dupe-keys": "warn",
"no-duplicate-case": "warn",
"no-empty-character-class": "warn",
"no-empty-pattern": "warn",
"no-empty-static-block": "warn",
"no-eval": "warn",
"no-ex-assign": "warn",
"no-extra-boolean-cast": "warn",
"no-func-assign": "warn",
"no-global-assign": "warn",
"no-import-assign": "warn",
"no-invalid-regexp": "warn",
"no-irregular-whitespace": "warn",
"no-loss-of-precision": "warn",
"no-new-native-nonconstructor": "warn",
"no-nonoctal-decimal-escape": "warn",
"no-obj-calls": "warn",
"no-self-assign": "warn",
"no-setter-return": "warn",
"no-shadow-restricted-names": "warn",
"no-sparse-arrays": "warn",
"no-this-before-super": "warn",
"no-unassigned-vars": "warn",
"no-unsafe-finally": "warn",
"no-unsafe-negation": "warn",
"no-unsafe-optional-chaining": "warn",
"no-unused-expressions": "warn",
"no-unused-labels": "warn",
"no-unused-private-class-members": "warn",
"no-unused-vars": [
"warn",
{
"caughtErrorsIgnorePattern": "^_",
"argsIgnorePattern": "^_"
}
],
"no-useless-backreference": "warn",
"no-useless-catch": "warn",
"no-useless-escape": "warn",
"no-useless-rename": "warn",
"no-with": "warn",
"require-yield": "warn",
"use-isnan": "warn",
"valid-typeof": "warn",
"oxc/bad-array-method-on-arguments": "warn",
"oxc/bad-char-at-comparison": "warn",
"oxc/bad-comparison-sequence": "warn",
"oxc/bad-min-max-func": "warn",
"oxc/bad-object-literal-comparison": "warn",
"oxc/bad-replace-all-arg": "warn",
"oxc/const-comparisons": "warn",
"oxc/double-comparisons": "warn",
"oxc/erasing-op": "warn",
"oxc/missing-throw": "warn",
"oxc/number-arg-out-of-range": "warn",
"oxc/only-used-in-recursion": "warn",
"oxc/uninvoked-array-callback": "warn",
"typescript/await-thenable": "warn",
"typescript/no-array-delete": "warn",
"typescript/no-base-to-string": "warn",
"typescript/no-duplicate-enum-values": "warn",
"typescript/no-duplicate-type-constituents": "warn",
"typescript/no-extra-non-null-assertion": "warn",
"typescript/no-floating-promises": "warn",
"typescript/no-for-in-array": "warn",
"typescript/no-implied-eval": "warn",
"typescript/no-meaningless-void-operator": "warn",
"typescript/no-misused-new": "warn",
"typescript/no-misused-spread": "warn",
"typescript/no-non-null-asserted-optional-chain": "warn",
"typescript/no-redundant-type-constituents": "warn",
"typescript/no-this-alias": "warn",
"typescript/no-unnecessary-parameter-property-assignment": "warn",
"typescript/no-unsafe-declaration-merging": "warn",
"typescript/no-unsafe-unary-minus": "warn",
"typescript/no-useless-empty-export": "warn",
"typescript/no-wrapper-object-types": "warn",
"typescript/prefer-as-const": "warn",
"typescript/require-array-sort-compare": "warn",
"typescript/restrict-template-expressions": "warn",
"typescript/triple-slash-reference": "warn",
"typescript/unbound-method": "warn",
"unicorn/no-await-in-promise-methods": "warn",
"unicorn/no-empty-file": "warn",
"unicorn/no-invalid-fetch-options": "warn",
"unicorn/no-invalid-remove-event-listener": "warn",
"unicorn/no-new-array": "warn",
"unicorn/no-single-promise-in-promise-methods": "warn",
"unicorn/no-thenable": "warn",
"unicorn/no-unnecessary-await": "warn",
"unicorn/no-useless-fallback-in-spread": "warn",
"unicorn/no-useless-length-check": "warn",
"unicorn/no-useless-spread": "warn",
"unicorn/prefer-set-size": "warn",
"unicorn/prefer-string-starts-ends-with": "warn",
"import/no-cycle": "error",
"eslint/no-console": ["warn", { "allow": ["warn", "error", "info"] }]
},
"settings": {
"jsx-a11y": {
"polymorphicPropName": null,
"components": {},
"attributes": {}
},
"next": {
"rootDir": []
},
"react": {
"formComponents": [],
"linkComponents": [],
"version": null
},
"jsdoc": {
"ignorePrivate": false,
"ignoreInternal": false,
"ignoreReplacesDocs": true,
"overrideReplacesDocs": true,
"augmentsExtendsReplacesDocs": false,
"implementsReplacesDocs": false,
"exemptDestructuredRootsFromChecks": false,
"tagNamePreference": {}
},
"vitest": {
"typecheck": false
}
},
"env": {
"builtin": true
},
"globals": {},
"ignorePatterns": ["**/api-client/**"]
}

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"
}

View file

@ -1,15 +1,30 @@
# AGENTS.md
## Important instructions
- Never create migration files manually. Always use the provided command to generate migrations
- If you realize an automated migration is incorrect, make sure to remove all the associated entries from the `_journal.json` and the newly created files located in `app/drizzle/` before re-generating the migration
- The dev server is running at http://localhost:3000. Username is `admin` and password is `password`
- 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
@ -24,7 +39,7 @@ bun run tsc
bun run test
# Run a specific test file
bunx dotenv-cli -e .env.test -- bun test --preload ./app/test/setup.ts path/to/test.ts
bunx dotenv-cli -e .env.test -- bunx --bun vitest run --project server path/to/test.ts
```
### Building
@ -55,7 +70,7 @@ bun run gen:api-client
```bash
# Format
bunx oxfmt format --write <path>
vp fmt <path> --write
# Lint
bun run lint

View file

@ -1,10 +1,8 @@
ARG BUN_VERSION="1.3.6"
FROM oven/bun:${BUN_VERSION}-alpine AS base
FROM oven/bun:1.3.14-alpine@sha256:5acc90a93e91ff07bf72aa90a7c9f0fa189765aec90b47bdbf2152d2196383c0 AS base
ARG RESTIC_VERSION="0.18.1"
ARG RCLONE_VERSION="1.73.0"
ARG SHOUTRRR_VERSION="0.13.2"
ARG RCLONE_VERSION="1.74.2"
ARG SHOUTRRR_VERSION="0.15.1"
ENV VITE_RESTIC_VERSION=${RESTIC_VERSION} \
VITE_RCLONE_VERSION=${RCLONE_VERSION} \
@ -12,7 +10,7 @@ ENV VITE_RESTIC_VERSION=${RESTIC_VERSION} \
RUN apk update --no-cache && \
apk upgrade --no-cache && \
apk add --no-cache davfs2=1.6.1-r2 openssh-client fuse3 sshfs tini
apk add --no-cache acl attr cifs-utils davfs2=1.6.1-r2 openssh-client fuse3 sshfs tini tzdata
ENTRYPOINT ["/sbin/tini", "-s", "--"]
@ -31,21 +29,30 @@ 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 -L -o rclone.zip "https://github.com/rclone/rclone/releases/download/v${RCLONE_VERSION}/rclone-v${RCLONE_VERSION}-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 -L -o shoutrrr.tar.gz "https://github.com/nicholas-fedor/shoutrrr/releases/download/v${SHOUTRRR_VERSION}/shoutrrr_linux_arm64v8_${SHOUTRRR_VERSION}.tar.gz"; \
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 -L -o rclone.zip "https://github.com/rclone/rclone/releases/download/v${RCLONE_VERSION}/rclone-v${RCLONE_VERSION}-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 -L -o shoutrrr.tar.gz "https://github.com/nicholas-fedor/shoutrrr/releases/download/v$SHOUTRRR_VERSION/shoutrrr_linux_amd64_${SHOUTRRR_VERSION}.tar.gz"; \
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-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
# ------------------------------
@ -63,8 +70,12 @@ COPY --from=deps /deps/rclone /usr/local/bin/rclone
COPY --from=deps /deps/shoutrrr /usr/local/bin/shoutrrr
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 bun install --frozen-lockfile --ignore-scripts
RUN VITE_GIT_HOOKS=0 bun install --frozen-lockfile --ignore-scripts
COPY . .
@ -84,11 +95,17 @@ 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 bun run build
RUN bun build apps/agent/src/index.ts --outfile .output/agent/index.mjs --target bun
FROM base AS production

162
README.md
View file

@ -15,23 +15,33 @@
</figure>
</div>
#### Join the community
[![Discord](https://img.shields.io/discord/1466834119873925263?label=discord&logo=discord)](https://discord.gg/XjgVyXrcEH)
> [!WARNING]
> 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 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.
<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>
## Intro
## 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, SFTP, 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
@ -40,7 +50,7 @@ In order to run Zerobyte, you need to have Docker and Docker Compose installed o
```yaml
services:
zerobyte:
image: ghcr.io/nicotsx/zerobyte:v0.28
image: ghcr.io/nicotsx/zerobyte:v0.38
container_name: zerobyte
restart: unless-stopped
cap_add:
@ -50,7 +60,7 @@ services:
devices:
- /dev/fuse:/dev/fuse
environment:
- TZ=Europe/Paris # Set your timezone here
- 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:
@ -59,10 +69,10 @@ services:
```
> [!WARNING]
> 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.
> 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.
> [!WARNING]
> Do not try to point `/var/lib/zerobyte` on a network share. You will face permission issues and strong performance degradation.
> 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:
@ -87,44 +97,72 @@ Once the container is running, you can access the web interface at `http://<your
Zerobyte can be customized using environment variables. Below are the available options:
### Environment Variables
### 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) |
| `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/Paris`). **Crucial for accurate backup scheduling.** | `UTC` |
| `TRUSTED_ORIGINS` | Comma-separated list of extra trusted origins for CORS (e.g., `http://localhost:3000,http://example.com`). | (none) |
| `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 rclone config directory inside the container. Change this if running as a non-root user. | `/root/.config/rclone` |
| 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) |
### Secret References
### Webhook and notification network policy
For enhanced security, Zerobyte supports dynamic secret resolution for sensitive fields (like passwords, access keys, etc.) in volume and repository configurations. Instead of storing the encrypted secret in the database, you can use one of the following prefixes:
Backup webhooks and outbound notification destinations that can target arbitrary network hosts are restricted by `WEBHOOK_ALLOWED_ORIGINS`.
- `env://VAR_NAME`: Reads the secret from the environment variable `VAR_NAME`.
- `file://SECRET_NAME`: Reads the secret from `/run/secrets/SECRET_NAME` (standard Docker Secrets path).
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`.
**Example:**
When configuring an S3 repository, you can set the Secret Access Key to `env://S3_SECRET_KEY` and then provide that variable in your `docker-compose.yml`.
This policy applies to:
### Simplified setup (No remote mounts)
- 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
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`:
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.28
image: ghcr.io/nicotsx/zerobyte:v0.38
container_name: zerobyte
restart: unless-stopped
ports:
- "4096:4096"
environment:
- TZ=Europe/Paris # Set your timezone here
- 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:
@ -136,8 +174,8 @@ services:
**Trade-offs:**
- ✅ Improved security by reducing container capabilities
- ✅ Support for local directories
- ✅ Keep support all repository types (local, S3, GCS, Azure, rclone)
- ✅ 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`.
@ -152,12 +190,12 @@ Zerobyte supports multiple volume backends including NFS, SMB, WebDAV, SFTP, and
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 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:
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:
zerobyte:
image: ghcr.io/nicotsx/zerobyte:v0.28
image: ghcr.io/nicotsx/zerobyte:v0.38
container_name: zerobyte
restart: unless-stopped
cap_add:
@ -167,7 +205,7 @@ services:
devices:
- /dev/fuse:/dev/fuse
environment:
- TZ=Europe/Paris
- 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:
@ -185,17 +223,17 @@ docker compose up -d
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/zerobyte/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. Zerobyte supports multiple storage backends for your backup repositories:
- **Local directories** - Store backups on local disk subfolder of `/var/lib/zerobyte/repositories/` or any other (mounted) path
- **S3-compatible storage** - Amazon S3, MinIO, Wasabi, DigitalOcean Spaces, etc.
- **Google Cloud Storage** - Google's cloud storage service
- **Azure Blob Storage** - Microsoft Azure storage
- **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.
@ -232,7 +270,7 @@ Zerobyte can use [rclone](https://rclone.org/) to support 40+ cloud storage prov
```diff
services:
zerobyte:
image: ghcr.io/nicotsx/zerobyte:v0.28
image: ghcr.io/nicotsx/zerobyte:v0.38
container_name: zerobyte
restart: unless-stopped
cap_add:
@ -242,7 +280,7 @@ Zerobyte can use [rclone](https://rclone.org/) to support 40+ cloud storage prov
devices:
- /dev/fuse:/dev/fuse
environment:
- TZ=Europe/Paris
- TZ=Europe/Zurich
- BASE_URL=http://localhost:4096 # URL you will use to access Zerobyte
volumes:
- /etc/localtime:/etc/localtime:ro
@ -250,7 +288,7 @@ Zerobyte can use [rclone](https://rclone.org/) to support 40+ cloud storage prov
+ - ~/.config/rclone:/root/.config/rclone:ro
```
> **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_CONF_DIR`:
> **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:
@ -286,24 +324,30 @@ When creating a backup job, you can specify the following settings:
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/zerobyte/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
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/zerobyte/blob/main/screenshots/restoring.png?raw=true)
![Restore UI](https://github.com/nicotsx/zerobyte/blob/main/screenshots/restoring.png?raw=true)
## Authentication
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:
### Cookie Security
### Users and roles
- **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.
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.
### Reverse Proxy Setup
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.
### Cookie security
- **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.
### Reverse proxy setup
If you're running Zerobyte behind a reverse proxy (Nginx, Traefik, Caddy, etc.):
@ -311,16 +355,18 @@ If you're running Zerobyte behind a reverse proxy (Nginx, Traefik, Caddy, etc.):
2. The app will automatically enable secure cookies based on the `https://` prefix
3. Ensure your proxy passes the `X-Forwarded-Proto` header
### Important Notes
### Important notes
- 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.
## Troubleshooting
For troubleshooting common issues, please refer to the [TROUBLESHOOTING.md](TROUBLESHOOTING.md) file.
## Third-Party Software
## Third-party software
This project includes the following third-party software components:
@ -341,10 +387,10 @@ Contributions by anyone are welcome! If you find a bug or have a feature request
## Development (no Docker)
You can run Zerobyte locally during development without Docker:
After installing Vite+ globally, you can run Zerobyte locally during development without Docker:
```bash
bun install
vp install
bun run dev
```
@ -358,10 +404,10 @@ RESTIC_PASS_FILE=./data/restic.pass
RESTIC_CACHE_DIR=./data/restic/cache
ZEROBYTE_REPOSITORIES_DIR=./data/repositories
ZEROBYTE_VOLUMES_DIR=./data/volumes
BASE_URL=http://localhost:4096
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.
- 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

View file

@ -283,9 +283,8 @@ If you're experiencing rclone issues, verify all of the following:
**Diagnosis:**
```bash
# Check if config is accessible inside container
docker exec zerobyte ls -la /root/.config/rclone/
docker exec zerobyte cat /root/.config/rclone/rclone.conf
# Check which config file rclone will use inside the container
docker exec zerobyte sh -lc 'echo HOME=$HOME; rclone config file'
```
**Solutions:**

View file

@ -37,7 +37,6 @@ body {
}
body {
@apply bg-[#131313];
min-height: 100dvh;
}
@ -67,6 +66,8 @@ body {
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-success: var(--success);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
@ -88,38 +89,40 @@ body {
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--card-header: oklch(0.922 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--background: oklch(0.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;
}
@ -144,6 +147,8 @@ body {
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: #ff543a;
--destructive-foreground: #fff;
--success: oklch(0.696 0.17 162.48);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
@ -174,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 {
@ -190,6 +195,17 @@ body {
}
}
.snapshot-scrollable::-webkit-scrollbar {
height: 6px;
}
.snapshot-scrollable::-webkit-scrollbar-track {
background: transparent;
}
.snapshot-scrollable::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 999px;
}
/* Hide built-in password reveal/clear controls (notably in Edge on Windows) */
[data-secret-input] input[type="password"]::-ms-reveal,
[data-secret-input] input[type="password"]::-ms-clear {

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()}` : ""}`,
);
},
},
);
});

View file

@ -4,8 +4,8 @@
import { type DefaultError, type InfiniteData, infiniteQueryOptions, queryOptions, type UseMutationOptions } from '@tanstack/react-query';
import { client } from '../client.gen';
import { browseFilesystem, cancelDoctor, createBackupSchedule, createNotificationDestination, createRepository, createVolume, deleteBackupSchedule, deleteNotificationDestination, deleteRepository, deleteSnapshot, deleteSnapshots, deleteSsoInvitation, deleteSsoProvider, deleteUserAccount, deleteVolume, devPanelExec, downloadResticPassword, dumpSnapshot, getAdminUsers, getBackupSchedule, getBackupScheduleForVolume, getDevPanel, getMirrorCompatibility, getNotificationDestination, 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, refreshSnapshots, reorderBackupSchedules, restoreSnapshot, runBackupNow, runForget, setRegistrationStatus, startDoctor, stopBackup, tagSnapshots, testConnection, testNotificationDestination, unlockRepository, unmountVolume, updateBackupSchedule, updateNotificationDestination, updateRepository, updateScheduleMirrors, updateScheduleNotifications, updateSsoProviderAutoLinking, updateVolume } from '../sdk.gen';
import type { BrowseFilesystemData, BrowseFilesystemResponse, CancelDoctorData, CancelDoctorResponse, CreateBackupScheduleData, CreateBackupScheduleResponse, CreateNotificationDestinationData, CreateNotificationDestinationResponse, CreateRepositoryData, CreateRepositoryResponse, CreateVolumeData, CreateVolumeResponse, DeleteBackupScheduleData, DeleteBackupScheduleResponse, DeleteNotificationDestinationData, DeleteNotificationDestinationResponse, DeleteRepositoryData, DeleteRepositoryResponse, DeleteSnapshotData, DeleteSnapshotResponse, DeleteSnapshotsData, DeleteSnapshotsResponse, DeleteSsoInvitationData, DeleteSsoProviderData, DeleteUserAccountData, DeleteVolumeData, DeleteVolumeResponse, DevPanelExecData, DevPanelExecResponse, DownloadResticPasswordData, DownloadResticPasswordResponse, DumpSnapshotData, DumpSnapshotResponse, GetAdminUsersData, GetAdminUsersResponse, GetBackupScheduleData, GetBackupScheduleForVolumeData, GetBackupScheduleForVolumeResponse, GetBackupScheduleResponse, GetDevPanelData, GetDevPanelResponse, GetMirrorCompatibilityData, GetMirrorCompatibilityResponse, GetNotificationDestinationData, GetNotificationDestinationResponse, GetPublicSsoProvidersData, GetPublicSsoProvidersResponse, GetRegistrationStatusData, GetRegistrationStatusResponse, GetRepositoryData, GetRepositoryResponse, GetRepositoryStatsData, GetRepositoryStatsResponse, GetScheduleMirrorsData, GetScheduleMirrorsResponse, GetScheduleNotificationsData, GetScheduleNotificationsResponse, GetSnapshotDetailsData, GetSnapshotDetailsResponse, GetSsoSettingsData, GetSsoSettingsResponse, GetStatusData, GetStatusResponse, GetSystemInfoData, GetSystemInfoResponse, GetUpdatesData, GetUpdatesResponse, GetUserDeletionImpactData, GetUserDeletionImpactResponse, GetVolumeData, GetVolumeResponse, HealthCheckVolumeData, HealthCheckVolumeResponse, ListBackupSchedulesData, ListBackupSchedulesResponse, ListFilesData, ListFilesResponse, ListNotificationDestinationsData, ListNotificationDestinationsResponse, ListRcloneRemotesData, ListRcloneRemotesResponse, ListRepositoriesData, ListRepositoriesResponse, ListSnapshotFilesData, ListSnapshotFilesResponse, ListSnapshotsData, ListSnapshotsResponse, ListVolumesData, ListVolumesResponse, MountVolumeData, MountVolumeResponse, RefreshSnapshotsData, RefreshSnapshotsResponse, ReorderBackupSchedulesData, ReorderBackupSchedulesResponse, RestoreSnapshotData, RestoreSnapshotResponse, RunBackupNowData, RunBackupNowResponse, RunForgetData, RunForgetResponse, SetRegistrationStatusData, SetRegistrationStatusResponse, StartDoctorData, StartDoctorResponse, StopBackupData, StopBackupResponse, TagSnapshotsData, TagSnapshotsResponse, TestConnectionData, TestConnectionResponse, TestNotificationDestinationData, TestNotificationDestinationResponse, UnlockRepositoryData, UnlockRepositoryResponse, UnmountVolumeData, UnmountVolumeResponse, UpdateBackupScheduleData, UpdateBackupScheduleResponse, UpdateNotificationDestinationData, UpdateNotificationDestinationResponse, UpdateRepositoryData, UpdateRepositoryResponse, UpdateScheduleMirrorsData, UpdateScheduleMirrorsResponse, UpdateScheduleNotificationsData, UpdateScheduleNotificationsResponse, UpdateSsoProviderAutoLinkingData, UpdateVolumeData, UpdateVolumeResponse } from '../types.gen';
import { browseFilesystem, cancelDoctor, createBackupSchedule, createNotificationDestination, createRepository, createVolume, deleteBackupSchedule, deleteNotificationDestination, deleteRepository, deleteSnapshot, deleteSnapshots, deleteSsoInvitation, deleteSsoProvider, deleteUserAccount, deleteVolume, 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';
import type { BrowseFilesystemData, BrowseFilesystemResponse, CancelDoctorData, CancelDoctorResponse, CreateBackupScheduleData, CreateBackupScheduleResponse, CreateNotificationDestinationData, CreateNotificationDestinationResponse, CreateRepositoryData, CreateRepositoryResponse, CreateVolumeData, CreateVolumeResponse, DeleteBackupScheduleData, DeleteBackupScheduleResponse, DeleteNotificationDestinationData, DeleteNotificationDestinationResponse, DeleteRepositoryData, DeleteRepositoryResponse, DeleteSnapshotData, DeleteSnapshotResponse, DeleteSnapshotsData, DeleteSnapshotsResponse, DeleteSsoInvitationData, DeleteSsoProviderData, DeleteUserAccountData, DeleteVolumeData, DeleteVolumeResponse, DownloadResticPasswordData, DownloadResticPasswordResponse, DumpSnapshotData, DumpSnapshotResponse, GetAdminUsersData, GetAdminUsersResponse, GetBackupProgressData, GetBackupProgressResponse, GetBackupScheduleData, GetBackupScheduleForVolumeData, GetBackupScheduleForVolumeResponse, GetBackupScheduleResponse, GetDevPanelData, GetDevPanelResponse, GetMirrorCompatibilityData, GetMirrorCompatibilityResponse, GetMirrorSyncStatusData, GetMirrorSyncStatusResponse, GetNotificationDestinationData, GetNotificationDestinationResponse, GetOrgMembersData, GetOrgMembersResponse, GetPublicSsoProvidersData, GetPublicSsoProvidersResponse, GetRegistrationStatusData, GetRegistrationStatusResponse, GetRepositoryData, GetRepositoryResponse, GetRepositoryStatsData, GetRepositoryStatsResponse, GetScheduleMirrorsData, GetScheduleMirrorsResponse, GetScheduleNotificationsData, GetScheduleNotificationsResponse, GetSnapshotDetailsData, GetSnapshotDetailsResponse, GetSsoSettingsData, GetSsoSettingsResponse, GetStatusData, GetStatusResponse, GetSystemInfoData, GetSystemInfoResponse, GetUpdatesData, GetUpdatesResponse, GetUserDeletionImpactData, GetUserDeletionImpactResponse, GetVolumeData, GetVolumeResponse, HealthCheckVolumeData, HealthCheckVolumeResponse, ListBackupSchedulesData, ListBackupSchedulesResponse, ListFilesData, ListFilesResponse, ListNotificationDestinationsData, ListNotificationDestinationsResponse, ListRcloneRemotesData, ListRcloneRemotesResponse, ListRepositoriesData, ListRepositoriesResponse, ListSnapshotFilesData, ListSnapshotFilesResponse, ListSnapshotsData, ListSnapshotsResponse, ListVolumesData, ListVolumesResponse, MountVolumeData, MountVolumeResponse, RefreshRepositoryStatsData, RefreshRepositoryStatsResponse, RefreshSnapshotsData, RefreshSnapshotsResponse, RemoveOrgMemberData, ReorderBackupSchedulesData, ReorderBackupSchedulesResponse, RestoreSnapshotData, RestoreSnapshotResponse, RunBackupNowData, RunBackupNowResponse, RunForgetData, RunForgetResponse, SetRegistrationStatusData, SetRegistrationStatusResponse, StartDoctorData, StartDoctorResponse, StopBackupData, StopBackupResponse, SyncMirrorData, SyncMirrorResponse, TagSnapshotsData, TagSnapshotsResponse, TestConnectionData, TestConnectionResponse, TestNotificationDestinationData, TestNotificationDestinationResponse, UnlockRepositoryData, UnlockRepositoryResponse, UnmountVolumeData, UnmountVolumeResponse, UpdateBackupScheduleData, UpdateBackupScheduleResponse, UpdateMemberRoleData, UpdateNotificationDestinationData, UpdateNotificationDestinationResponse, UpdateRepositoryData, UpdateRepositoryResponse, UpdateScheduleMirrorsData, UpdateScheduleMirrorsResponse, UpdateScheduleNotificationsData, UpdateScheduleNotificationsResponse, UpdateSsoProviderAutoLinkingData, UpdateVolumeData, UpdateVolumeResponse } from '../types.gen';
export type QueryKey<TOptions extends Options> = [
Pick<TOptions, 'baseUrl' | 'body' | 'headers' | 'path' | 'query'> & {
@ -58,6 +58,111 @@ export const getStatusOptions = (options?: Options<GetStatusData>) => queryOptio
queryKey: getStatusQueryKey(options)
});
export const getAdminUsersQueryKey = (options?: Options<GetAdminUsersData>) => createQueryKey('getAdminUsers', options);
/**
* List admin users for settings management
*/
export const getAdminUsersOptions = (options?: Options<GetAdminUsersData>) => queryOptions<GetAdminUsersResponse, DefaultError, GetAdminUsersResponse, ReturnType<typeof getAdminUsersQueryKey>>({
queryFn: async ({ queryKey, signal }) => {
const { data } = await getAdminUsers({
...options,
...queryKey[0],
signal,
throwOnError: true
});
return data;
},
queryKey: getAdminUsersQueryKey(options)
});
/**
* Delete an account linked to a user
*/
export const deleteUserAccountMutation = (options?: Partial<Options<DeleteUserAccountData>>): UseMutationOptions<unknown, DefaultError, Options<DeleteUserAccountData>> => {
const mutationOptions: UseMutationOptions<unknown, DefaultError, Options<DeleteUserAccountData>> = {
mutationFn: async (fnOptions) => {
const { data } = await deleteUserAccount({
...options,
...fnOptions,
throwOnError: true
});
return data;
}
};
return mutationOptions;
};
export const getUserDeletionImpactQueryKey = (options: Options<GetUserDeletionImpactData>) => createQueryKey('getUserDeletionImpact', options);
/**
* Get impact of deleting a user
*/
export const getUserDeletionImpactOptions = (options: Options<GetUserDeletionImpactData>) => queryOptions<GetUserDeletionImpactResponse, DefaultError, GetUserDeletionImpactResponse, ReturnType<typeof getUserDeletionImpactQueryKey>>({
queryFn: async ({ queryKey, signal }) => {
const { data } = await getUserDeletionImpact({
...options,
...queryKey[0],
signal,
throwOnError: true
});
return data;
},
queryKey: getUserDeletionImpactQueryKey(options)
});
export const getOrgMembersQueryKey = (options?: Options<GetOrgMembersData>) => createQueryKey('getOrgMembers', options);
/**
* Get members of the active organization
*/
export const getOrgMembersOptions = (options?: Options<GetOrgMembersData>) => queryOptions<GetOrgMembersResponse, DefaultError, GetOrgMembersResponse, ReturnType<typeof getOrgMembersQueryKey>>({
queryFn: async ({ queryKey, signal }) => {
const { data } = await getOrgMembers({
...options,
...queryKey[0],
signal,
throwOnError: true
});
return data;
},
queryKey: getOrgMembersQueryKey(options)
});
/**
* Update a member's role in the active organization
*/
export const updateMemberRoleMutation = (options?: Partial<Options<UpdateMemberRoleData>>): UseMutationOptions<unknown, DefaultError, Options<UpdateMemberRoleData>> => {
const mutationOptions: UseMutationOptions<unknown, DefaultError, Options<UpdateMemberRoleData>> = {
mutationFn: async (fnOptions) => {
const { data } = await updateMemberRole({
...options,
...fnOptions,
throwOnError: true
});
return data;
}
};
return mutationOptions;
};
/**
* Remove a member from the active organization
*/
export const removeOrgMemberMutation = (options?: Partial<Options<RemoveOrgMemberData>>): UseMutationOptions<unknown, DefaultError, Options<RemoveOrgMemberData>> => {
const mutationOptions: UseMutationOptions<unknown, DefaultError, Options<RemoveOrgMemberData>> = {
mutationFn: async (fnOptions) => {
const { data } = await removeOrgMember({
...options,
...fnOptions,
throwOnError: true
});
return data;
}
};
return mutationOptions;
};
export const getPublicSsoProvidersQueryKey = (options?: Options<GetPublicSsoProvidersData>) => createQueryKey('getPublicSsoProviders', options);
/**
@ -145,59 +250,6 @@ export const deleteSsoInvitationMutation = (options?: Partial<Options<DeleteSsoI
return mutationOptions;
};
export const getAdminUsersQueryKey = (options?: Options<GetAdminUsersData>) => createQueryKey('getAdminUsers', options);
/**
* List admin users for settings management
*/
export const getAdminUsersOptions = (options?: Options<GetAdminUsersData>) => queryOptions<GetAdminUsersResponse, DefaultError, GetAdminUsersResponse, ReturnType<typeof getAdminUsersQueryKey>>({
queryFn: async ({ queryKey, signal }) => {
const { data } = await getAdminUsers({
...options,
...queryKey[0],
signal,
throwOnError: true
});
return data;
},
queryKey: getAdminUsersQueryKey(options)
});
/**
* Delete an account linked to a user
*/
export const deleteUserAccountMutation = (options?: Partial<Options<DeleteUserAccountData>>): UseMutationOptions<unknown, DefaultError, Options<DeleteUserAccountData>> => {
const mutationOptions: UseMutationOptions<unknown, DefaultError, Options<DeleteUserAccountData>> = {
mutationFn: async (fnOptions) => {
const { data } = await deleteUserAccount({
...options,
...fnOptions,
throwOnError: true
});
return data;
}
};
return mutationOptions;
};
export const getUserDeletionImpactQueryKey = (options: Options<GetUserDeletionImpactData>) => createQueryKey('getUserDeletionImpact', options);
/**
* Get impact of deleting a user
*/
export const getUserDeletionImpactOptions = (options: Options<GetUserDeletionImpactData>) => queryOptions<GetUserDeletionImpactResponse, DefaultError, GetUserDeletionImpactResponse, ReturnType<typeof getUserDeletionImpactQueryKey>>({
queryFn: async ({ queryKey, signal }) => {
const { data } = await getUserDeletionImpact({
...options,
...queryKey[0],
signal,
throwOnError: true
});
return data;
},
queryKey: getUserDeletionImpactQueryKey(options)
});
export const listVolumesQueryKey = (options?: Options<ListVolumesData>) => createQueryKey('listVolumes', options);
/**
@ -405,7 +457,7 @@ export const listFilesInfiniteQueryKey = (options: Options<ListFilesData>): Quer
/**
* List files in a volume directory
*/
export const listFilesInfiniteOptions = (options: Options<ListFilesData>) => infiniteQueryOptions<ListFilesResponse, DefaultError, InfiniteData<ListFilesResponse>, QueryKey<Options<ListFilesData>>, string | Pick<QueryKey<Options<ListFilesData>>[0], 'body' | 'headers' | 'path' | 'query'>>(
export const listFilesInfiniteOptions = (options: Options<ListFilesData>) => infiniteQueryOptions<ListFilesResponse, DefaultError, InfiniteData<ListFilesResponse>, QueryKey<Options<ListFilesData>>, number | Pick<QueryKey<Options<ListFilesData>>[0], 'body' | 'headers' | 'path' | 'query'>>(
// @ts-ignore
{
queryFn: async ({ pageParam, queryKey, signal }) => {
@ -568,6 +620,23 @@ export const getRepositoryStatsOptions = (options: Options<GetRepositoryStatsDat
queryKey: getRepositoryStatsQueryKey(options)
});
/**
* Refresh repository storage and compression statistics
*/
export const refreshRepositoryStatsMutation = (options?: Partial<Options<RefreshRepositoryStatsData>>): UseMutationOptions<RefreshRepositoryStatsResponse, DefaultError, Options<RefreshRepositoryStatsData>> => {
const mutationOptions: UseMutationOptions<RefreshRepositoryStatsResponse, DefaultError, Options<RefreshRepositoryStatsData>> = {
mutationFn: async (fnOptions) => {
const { data } = await refreshRepositoryStats({
...options,
...fnOptions,
throwOnError: true
});
return data;
}
};
return mutationOptions;
};
/**
* Delete multiple snapshots from a repository
*/
@ -678,7 +747,7 @@ export const listSnapshotFilesInfiniteQueryKey = (options: Options<ListSnapshotF
/**
* List files and directories in a snapshot
*/
export const listSnapshotFilesInfiniteOptions = (options: Options<ListSnapshotFilesData>) => infiniteQueryOptions<ListSnapshotFilesResponse, DefaultError, InfiniteData<ListSnapshotFilesResponse>, QueryKey<Options<ListSnapshotFilesData>>, string | Pick<QueryKey<Options<ListSnapshotFilesData>>[0], 'body' | 'headers' | 'path' | 'query'>>(
export const listSnapshotFilesInfiniteOptions = (options: Options<ListSnapshotFilesData>) => infiniteQueryOptions<ListSnapshotFilesResponse, DefaultError, InfiniteData<ListSnapshotFilesResponse>, QueryKey<Options<ListSnapshotFilesData>>, number | Pick<QueryKey<Options<ListSnapshotFilesData>>[0], 'body' | 'headers' | 'path' | 'query'>>(
// @ts-ignore
{
queryFn: async ({ pageParam, queryKey, signal }) => {
@ -803,23 +872,6 @@ export const tagSnapshotsMutation = (options?: Partial<Options<TagSnapshotsData>
return mutationOptions;
};
/**
* Execute a restic command against a repository (dev panel only)
*/
export const devPanelExecMutation = (options?: Partial<Options<DevPanelExecData>>): UseMutationOptions<DevPanelExecResponse, DefaultError, Options<DevPanelExecData>> => {
const mutationOptions: UseMutationOptions<DevPanelExecResponse, DefaultError, Options<DevPanelExecData>> = {
mutationFn: async (fnOptions) => {
const { data } = await devPanelExec({
...options,
...fnOptions,
throwOnError: true
});
return data;
}
};
return mutationOptions;
};
export const listBackupSchedulesQueryKey = (options?: Options<ListBackupSchedulesData>) => createQueryKey('listBackupSchedules', options);
/**
@ -1046,6 +1098,41 @@ export const updateScheduleMirrorsMutation = (options?: Partial<Options<UpdateSc
return mutationOptions;
};
export const getMirrorSyncStatusQueryKey = (options: Options<GetMirrorSyncStatusData>) => createQueryKey('getMirrorSyncStatus', options);
/**
* Get sync status for a specific mirror, including missing snapshots
*/
export const getMirrorSyncStatusOptions = (options: Options<GetMirrorSyncStatusData>) => queryOptions<GetMirrorSyncStatusResponse, DefaultError, GetMirrorSyncStatusResponse, ReturnType<typeof getMirrorSyncStatusQueryKey>>({
queryFn: async ({ queryKey, signal }) => {
const { data } = await getMirrorSyncStatus({
...options,
...queryKey[0],
signal,
throwOnError: true
});
return data;
},
queryKey: getMirrorSyncStatusQueryKey(options)
});
/**
* Sync selected snapshots to a specific mirror repository
*/
export const syncMirrorMutation = (options?: Partial<Options<SyncMirrorData>>): UseMutationOptions<SyncMirrorResponse, DefaultError, Options<SyncMirrorData>> => {
const mutationOptions: UseMutationOptions<SyncMirrorResponse, DefaultError, Options<SyncMirrorData>> = {
mutationFn: async (fnOptions) => {
const { data } = await syncMirror({
...options,
...fnOptions,
throwOnError: true
});
return data;
}
};
return mutationOptions;
};
export const getMirrorCompatibilityQueryKey = (options: Options<GetMirrorCompatibilityData>) => createQueryKey('getMirrorCompatibility', options);
/**
@ -1081,6 +1168,24 @@ export const reorderBackupSchedulesMutation = (options?: Partial<Options<Reorder
return mutationOptions;
};
export const getBackupProgressQueryKey = (options: Options<GetBackupProgressData>) => createQueryKey('getBackupProgress', options);
/**
* Get the last known progress for a currently running backup. Returns null if no progress has been reported yet.
*/
export const getBackupProgressOptions = (options: Options<GetBackupProgressData>) => queryOptions<GetBackupProgressResponse, DefaultError, GetBackupProgressResponse, ReturnType<typeof getBackupProgressQueryKey>>({
queryFn: async ({ queryKey, signal }) => {
const { data } = await getBackupProgress({
...options,
...queryKey[0],
signal,
throwOnError: true
});
return data;
},
queryKey: getBackupProgressQueryKey(options)
});
export const listNotificationDestinationsQueryKey = (options?: Options<ListNotificationDestinationsData>) => createQueryKey('listNotificationDestinations', options);
/**

View file

@ -1,3 +1,4 @@
// @ts-nocheck
// This file is auto-generated by @hey-api/openapi-ts
import { createSseClient } from '../core/serverSentEvents.gen';
@ -31,20 +32,24 @@ export const createClient = (config: Config = {}): Client => {
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) {
@ -52,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
@ -60,178 +65,162 @@ 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 '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':
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) =>
@ -242,7 +231,6 @@ export const createClient = (config: Config = {}): Client => {
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);
@ -258,8 +246,10 @@ export const createClient = (config: Config = {}): Client => {
});
};
const _buildUrl: Client['buildUrl'] = (options) => buildUrl({ ..._config, ...options });
return {
buildUrl,
buildUrl: _buildUrl,
connect: makeMethodFn('CONNECT'),
delete: makeMethodFn('DELETE'),
get: makeMethodFn('GET'),

View file

@ -1,3 +1,4 @@
// @ts-nocheck
// This file is auto-generated by @hey-api/openapi-ts
export type { Auth } from "../core/auth.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';
@ -66,6 +67,7 @@ export interface RequestOptions<
}>,
Pick<
ServerSentEventsOptions<TData>,
| 'onRequest'
| 'onSseError'
| 'onSseEvent'
| 'sseDefaultRetryDelay'
@ -92,6 +94,7 @@ export interface ResolvedRequestOptions<
ThrowOnError extends boolean = boolean,
Url extends string = string,
> extends RequestOptions<unknown, TResponseStyle, ThrowOnError, Url> {
headers: Headers;
serializedBody?: string;
}
@ -125,8 +128,10 @@ export type RequestResult<
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;
}
>;
@ -151,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 = <

View file

@ -1,3 +1,4 @@
// @ts-nocheck
// This file is auto-generated by @hey-api/openapi-ts
import { getAuthToken } from '../core/auth.gen';
@ -118,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;
}
@ -154,7 +153,7 @@ export const setAuthParams = async ({
break;
}
}
};
}
export const buildUrl: Client['buildUrl'] = (options) =>
getUrl({
@ -204,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,
@ -218,8 +217,10 @@ 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>;

View file

@ -1,3 +1,4 @@
// @ts-nocheck
// This file is auto-generated by @hey-api/openapi-ts
export type AuthToken = string | undefined;

View file

@ -1,10 +1,11 @@
// @ts-nocheck
// This file is auto-generated by @hey-api/openapi-ts
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;
@ -39,12 +40,10 @@ const serializeUrlSearchParamsPair = (data: URLSearchParams, key: string, value:
};
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;
}
@ -60,15 +59,15 @@ export const formDataBodySerializer = {
};
export const jsonBodySerializer = {
bodySerializer: <T>(body: T): string =>
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,7 +97,7 @@ 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];
}
}

View file

@ -1,3 +1,4 @@
// @ts-nocheck
// This file is auto-generated by @hey-api/openapi-ts
interface SerializeOptions<T> extends SerializePrimitiveOptions, SerializerOptions<T> {}

View file

@ -1,3 +1,4 @@
// @ts-nocheck
// This file is auto-generated by @hey-api/openapi-ts
/**

View file

@ -1,3 +1,4 @@
// @ts-nocheck
// This file is auto-generated by @hey-api/openapi-ts
import type { Config } from './types.gen';
@ -79,7 +80,7 @@ export type ServerSentEventsResult<TData = unknown, TReturn = void, TNext = unkn
>;
};
export const createSseClient = <TData = unknown>({
export function createSseClient<TData = unknown>({
onRequest,
onSseError,
onSseEvent,
@ -91,7 +92,7 @@ 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)));
@ -155,8 +156,7 @@ export const createSseClient = <TData = unknown>({
const { done, value } = await reader.read();
if (done) break;
buffer += value;
// Normalize line endings: CRLF -> LF, then CR -> LF
buffer = buffer.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
buffer = buffer.replace(/\r\n?/g, '\n'); // normalize line endings
const chunks = buffer.split('\n\n');
buffer = chunks.pop() ?? '';
@ -240,4 +240,4 @@ export const createSseClient = <TData = unknown>({
const stream = createStream();
return { stream };
};
}

View file

@ -1,3 +1,4 @@
// @ts-nocheck
// This file is auto-generated by @hey-api/openapi-ts
import type { Auth, AuthToken } from './auth.gen';
@ -80,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>;
/**

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';
@ -126,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

@ -21,11 +21,14 @@ export {
downloadResticPassword,
dumpSnapshot,
getAdminUsers,
getBackupProgress,
getBackupSchedule,
getBackupScheduleForVolume,
getDevPanel,
getMirrorCompatibility,
getMirrorSyncStatus,
getNotificationDestination,
getOrgMembers,
getPublicSsoProviders,
getRegistrationStatus,
getRepository,
@ -50,7 +53,9 @@ export {
listVolumes,
mountVolume,
type Options,
refreshRepositoryStats,
refreshSnapshots,
removeOrgMember,
reorderBackupSchedules,
restoreSnapshot,
runBackupNow,
@ -58,12 +63,14 @@ export {
setRegistrationStatus,
startDoctor,
stopBackup,
syncMirror,
tagSnapshots,
testConnection,
testNotificationDestination,
unlockRepository,
unmountVolume,
updateBackupSchedule,
updateMemberRole,
updateNotificationDestination,
updateRepository,
updateScheduleMirrors,
@ -133,6 +140,9 @@ export type {
GetAdminUsersData,
GetAdminUsersResponse,
GetAdminUsersResponses,
GetBackupProgressData,
GetBackupProgressResponse,
GetBackupProgressResponses,
GetBackupScheduleData,
GetBackupScheduleForVolumeData,
GetBackupScheduleForVolumeResponse,
@ -145,10 +155,16 @@ export type {
GetMirrorCompatibilityData,
GetMirrorCompatibilityResponse,
GetMirrorCompatibilityResponses,
GetMirrorSyncStatusData,
GetMirrorSyncStatusResponse,
GetMirrorSyncStatusResponses,
GetNotificationDestinationData,
GetNotificationDestinationErrors,
GetNotificationDestinationResponse,
GetNotificationDestinationResponses,
GetOrgMembersData,
GetOrgMembersResponse,
GetOrgMembersResponses,
GetPublicSsoProvidersData,
GetPublicSsoProvidersResponse,
GetPublicSsoProvidersResponses,
@ -220,9 +236,15 @@ export type {
MountVolumeData,
MountVolumeResponse,
MountVolumeResponses,
RefreshRepositoryStatsData,
RefreshRepositoryStatsResponse,
RefreshRepositoryStatsResponses,
RefreshSnapshotsData,
RefreshSnapshotsResponse,
RefreshSnapshotsResponses,
RemoveOrgMemberData,
RemoveOrgMemberErrors,
RemoveOrgMemberResponses,
ReorderBackupSchedulesData,
ReorderBackupSchedulesResponse,
ReorderBackupSchedulesResponses,
@ -246,6 +268,10 @@ export type {
StopBackupErrors,
StopBackupResponse,
StopBackupResponses,
SyncMirrorData,
SyncMirrorErrors,
SyncMirrorResponse,
SyncMirrorResponses,
TagSnapshotsData,
TagSnapshotsResponse,
TagSnapshotsResponses,
@ -265,6 +291,9 @@ export type {
UpdateBackupScheduleData,
UpdateBackupScheduleResponse,
UpdateBackupScheduleResponses,
UpdateMemberRoleData,
UpdateMemberRoleErrors,
UpdateMemberRoleResponses,
UpdateNotificationDestinationData,
UpdateNotificationDestinationErrors,
UpdateNotificationDestinationResponse,

View file

@ -3,9 +3,9 @@
import type { Client, Options as Options2, TDataShape } from './client';
import { client } from './client.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, DevPanelExecResponses, DownloadResticPasswordData, DownloadResticPasswordResponses, DumpSnapshotData, DumpSnapshotResponses, GetAdminUsersData, GetAdminUsersResponses, GetBackupScheduleData, GetBackupScheduleForVolumeData, GetBackupScheduleForVolumeResponses, GetBackupScheduleResponses, GetDevPanelData, GetDevPanelResponses, GetMirrorCompatibilityData, GetMirrorCompatibilityResponses, GetNotificationDestinationData, GetNotificationDestinationErrors, GetNotificationDestinationResponses, 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, RefreshSnapshotsData, RefreshSnapshotsResponses, ReorderBackupSchedulesData, ReorderBackupSchedulesResponses, RestoreSnapshotData, RestoreSnapshotResponses, RunBackupNowData, RunBackupNowResponses, RunForgetData, RunForgetResponses, SetRegistrationStatusData, SetRegistrationStatusResponses, StartDoctorData, StartDoctorErrors, StartDoctorResponses, StopBackupData, StopBackupErrors, StopBackupResponses, TagSnapshotsData, TagSnapshotsResponses, TestConnectionData, TestConnectionResponses, TestNotificationDestinationData, TestNotificationDestinationErrors, TestNotificationDestinationResponses, UnlockRepositoryData, UnlockRepositoryResponses, UnmountVolumeData, UnmountVolumeResponses, UpdateBackupScheduleData, UpdateBackupScheduleResponses, UpdateNotificationDestinationData, UpdateNotificationDestinationErrors, UpdateNotificationDestinationResponses, UpdateRepositoryData, UpdateRepositoryErrors, UpdateRepositoryResponses, UpdateScheduleMirrorsData, UpdateScheduleMirrorsResponses, UpdateScheduleNotificationsData, UpdateScheduleNotificationsResponses, UpdateSsoProviderAutoLinkingData, UpdateSsoProviderAutoLinkingErrors, UpdateSsoProviderAutoLinkingResponses, 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
@ -24,6 +24,43 @@ export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends
*/
export const getStatus = <ThrowOnError extends boolean = false>(options?: Options<GetStatusData, ThrowOnError>) => (options?.client ?? client).get<GetStatusResponses, unknown, ThrowOnError>({ url: '/api/v1/auth/status', ...options });
/**
* List admin users for settings management
*/
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
*/
@ -56,21 +93,6 @@ export const updateSsoProviderAutoLinking = <ThrowOnError extends boolean = fals
*/
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 admin users for settings management
*/
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 });
/**
* List all volumes
*/
@ -79,24 +101,24 @@ export const listVolumes = <ThrowOnError extends boolean = false>(options?: Opti
/**
* Create a new volume
*/
export const createVolume = <ThrowOnError extends boolean = false>(options?: Options<CreateVolumeData, ThrowOnError>) => (options?.client ?? client).post<CreateVolumeResponses, unknown, ThrowOnError>({
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
...options.headers
}
});
/**
* Test connection to backend
*/
export const testConnection = <ThrowOnError extends boolean = false>(options?: Options<TestConnectionData, ThrowOnError>) => (options?.client ?? client).post<TestConnectionResponses, unknown, ThrowOnError>({
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
...options.headers
}
});
@ -155,12 +177,12 @@ export const listRepositories = <ThrowOnError extends boolean = false>(options?:
/**
* Create a new restic repository
*/
export const createRepository = <ThrowOnError extends boolean = false>(options?: Options<CreateRepositoryData, ThrowOnError>) => (options?.client ?? client).post<CreateRepositoryResponses, unknown, ThrowOnError>({
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
...options.headers
}
});
@ -196,6 +218,11 @@ export const updateRepository = <ThrowOnError extends boolean = false>(options:
*/
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
*/
@ -280,7 +307,7 @@ export const tagSnapshots = <ThrowOnError extends boolean = false>(options: Opti
/**
* Execute a restic command against a repository (dev panel only)
*/
export const devPanelExec = <ThrowOnError extends boolean = false>(options: Options<DevPanelExecData, ThrowOnError>) => (options.client ?? client).sse.post<DevPanelExecResponses, DevPanelExecErrors, ThrowOnError>({
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: {
@ -297,12 +324,12 @@ export const listBackupSchedules = <ThrowOnError extends boolean = false>(option
/**
* Create a new backup schedule for a volume
*/
export const createBackupSchedule = <ThrowOnError extends boolean = false>(options?: Options<CreateBackupScheduleData, ThrowOnError>) => (options?.client ?? client).post<CreateBackupScheduleResponses, unknown, ThrowOnError>({
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
...options.headers
}
});
@ -382,6 +409,23 @@ export const updateScheduleMirrors = <ThrowOnError extends boolean = false>(opti
}
});
/**
* 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
*/
@ -390,15 +434,20 @@ export const getMirrorCompatibility = <ThrowOnError extends boolean = false>(opt
/**
* 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>({
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
...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
*/
@ -407,12 +456,12 @@ export const listNotificationDestinations = <ThrowOnError extends boolean = fals
/**
* Create a new notification destination
*/
export const createNotificationDestination = <ThrowOnError extends boolean = false>(options?: Options<CreateNotificationDestinationData, ThrowOnError>) => (options?.client ?? client).post<CreateNotificationDestinationResponses, unknown, ThrowOnError>({
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
...options.headers
}
});
@ -461,24 +510,24 @@ export const getRegistrationStatus = <ThrowOnError extends boolean = false>(opti
/**
* 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>({
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
...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>({
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
...options.headers
}
});

File diff suppressed because it is too large Load diff

View file

@ -1,8 +1,48 @@
/** biome-ignore-all lint/style/noNonNullAssertion: Testing file - non-null assertions are acceptable here */
import { expect, test, describe } from "bun:test";
import { render, screen, fireEvent } from "@testing-library/react";
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" },
@ -145,6 +185,19 @@ describe("FileTree Pagination", () => {
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", () => {
@ -157,154 +210,67 @@ describe("FileTree Selection Logic", () => {
{ name: "upload", path: "/root/photos/upload", type: "folder" },
];
test("selecting a folder simplifies to parent if it's the only child", async () => {
let currentSelection = new Set<string>();
const onSelectionChange = (selection: Set<string>) => {
currentSelection = selection;
};
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))} />);
render(
<FileTree
files={testFiles}
withCheckboxes={true}
selectedPaths={currentSelection}
onSelectionChange={onSelectionChange}
expandedFolders={new Set(testFiles.map((f) => f.path))}
/>,
);
fireEvent.click(getCheckboxFor("photos"));
const photosCheckbox = screen.getByText("photos").parentElement?.querySelector('button[role="checkbox"]');
expect(photosCheckbox).toBeTruthy();
fireEvent.click(photosCheckbox!);
expect(currentSelection.has("/root")).toBe(true);
expect(currentSelection.size).toBe(1);
expect(getSelectedPaths()).toEqual(["/root"]);
});
test("unselecting a child removes the parent from selection", async () => {
let currentSelection = new Set<string>(["/root"]);
const onSelectionChange = (selection: Set<string>) => {
currentSelection = selection;
};
test("unselecting a child removes the parent from selection", () => {
render(
<FileTree
<FileTreeSelection
files={testFiles}
withCheckboxes={true}
selectedPaths={currentSelection}
onSelectionChange={onSelectionChange}
initialSelectedPaths={["/root"]}
expandedFolders={new Set(testFiles.map((f) => f.path))}
/>,
);
const libraryCheckbox = screen.getByText("library").parentElement?.querySelector('button[role="checkbox"]');
fireEvent.click(libraryCheckbox!);
fireEvent.click(getCheckboxFor("library"));
expect(currentSelection.has("/root")).toBe(false);
expect(currentSelection.has("/root/photos")).toBe(false);
expect(currentSelection.has("/root/photos/backups")).toBe(true);
expect(currentSelection.has("/root/photos/profile")).toBe(true);
expect(currentSelection.has("/root/photos/upload")).toBe(true);
expect(currentSelection.size).toBe(3);
expect(getSelectedPaths()).toEqual(["/root/photos/backups", "/root/photos/profile", "/root/photos/upload"]);
});
test("recursive simplification when all children are selected", async () => {
let currentSelection = new Set<string>();
const onSelectionChange = (selection: Set<string>) => {
currentSelection = selection;
};
const { rerender } = render(
<FileTree
files={testFiles}
withCheckboxes={true}
selectedPaths={currentSelection}
onSelectionChange={onSelectionChange}
expandedFolders={new Set(testFiles.map((f) => f.path))}
/>,
);
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) {
const checkbox = screen.getByText(name).parentElement?.querySelector('button[role="checkbox"]');
fireEvent.click(checkbox!);
rerender(
<FileTree
files={testFiles}
withCheckboxes={true}
selectedPaths={currentSelection}
onSelectionChange={onSelectionChange}
expandedFolders={new Set(testFiles.map((f) => f.path))}
/>,
);
fireEvent.click(getCheckboxFor(name));
}
expect(currentSelection.has("/root")).toBe(true);
expect(currentSelection.size).toBe(1);
expect(getSelectedPaths()).toEqual(["/root"]);
});
test("does not simplify to parent if not all children are selected", async () => {
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" },
];
let currentSelection = new Set<string>();
const onSelectionChange = (selection: Set<string>) => {
currentSelection = selection;
};
render(<FileTreeSelection files={multipleFiles} expandedFolders={new Set(multipleFiles.map((f) => f.path))} />);
render(
<FileTree
files={multipleFiles}
withCheckboxes={true}
selectedPaths={currentSelection}
onSelectionChange={onSelectionChange}
expandedFolders={new Set(multipleFiles.map((f) => f.path))}
/>,
);
fireEvent.click(getCheckboxFor("child1"));
const child1Checkbox = screen.getByText("child1").parentElement?.querySelector('button[role="checkbox"]');
fireEvent.click(child1Checkbox!);
expect(currentSelection.has("/root/child1")).toBe(true);
expect(currentSelection.has("/root")).toBe(false);
expect(currentSelection.size).toBe(1);
expect(getSelectedPaths()).toEqual(["/root/child1"]);
});
test("simplifies existing deep paths when parent is selected", async () => {
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" },
];
let currentSelection = new Set<string>(["/hello", "/hello_prev", "/service/app/data/upload"]);
const onSelectionChange = (selection: Set<string>) => {
currentSelection = selection;
};
render(
<FileTree
files={files}
withCheckboxes={true}
selectedPaths={currentSelection}
onSelectionChange={onSelectionChange}
/>,
<FileTreeSelection files={files} initialSelectedPaths={["/hello", "/hello_prev", "/service/app/data/upload"]} />,
);
const serviceCheckbox = screen.getByText("service").parentElement?.querySelector('button[role="checkbox"]');
expect(serviceCheckbox).toBeTruthy();
fireEvent.click(getCheckboxFor("service"));
fireEvent.click(serviceCheckbox!);
expect(currentSelection.has("/service")).toBe(true);
expect(currentSelection.has("/service/app/data/upload")).toBe(false);
expect(currentSelection.size).toBe(3); // /hello, /hello_prev, /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,3 +1,4 @@
import { Fragment } from "react";
import { useMatches, Link } from "@tanstack/react-router";
import {
Breadcrumb,
@ -41,7 +42,7 @@ export function AppBreadcrumb() {
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>
@ -52,7 +53,7 @@ export function AppBreadcrumb() {
)}
</BreadcrumbItem>
{!isLast && <BreadcrumbSeparator />}
</div>
</Fragment>
);
})}
</BreadcrumbList>

View file

@ -1,4 +1,4 @@
import { Bell, CalendarClock, Database, HardDrive, Settings } from "lucide-react";
import { Bell, CalendarClock, Database, HardDrive, Settings, ShieldCheck } from "lucide-react";
import { useState } from "react";
import {
Sidebar,
@ -10,6 +10,7 @@ import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarSeparator,
useSidebar,
} from "~/client/components/ui/sidebar";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "~/client/components/ui/tooltip";
@ -49,11 +50,17 @@ const items = [
},
];
export function AppSidebar() {
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"
@ -62,12 +69,12 @@ export function AppSidebar() {
return (
<Sidebar variant="inset" collapsible="icon" className="p-0">
<SidebarHeader className="bg-card-header border-b border-border/50 hidden md:flex h-16.25 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">
<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 -ml-1", {
"opacity-0 w-0 overflow-hidden ": state === "collapsed",
"opacity-0 w-0 overflow-hidden ": isCollapsed,
})}
>
Zerobyte
@ -83,18 +90,43 @@ export function AppSidebar() {
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<SidebarMenuButton asChild>
<Link to={item.url} onClick={() => isMobile && setOpenMobile(false)}>
<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>
</>
)}
</Link>
</SidebarMenuButton>
</TooltipTrigger>
<TooltipContent side="right" className={cn({ hidden: state !== "collapsed" })}>
<TooltipContent side="right" className={cn({ hidden: !isCollapsed })}>
<p>{item.title}</p>
</TooltipContent>
</Tooltip>
@ -104,8 +136,65 @@ 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-border/50">
<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}>

View file

@ -1,14 +1,13 @@
import { Card, CardContent } from "~/client/components/ui/card";
import { ByteSize } from "~/client/components/bytes-size";
import type { ResticSnapshotSummaryDto } from "~/schemas/restic-dto";
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 formatCount = (value: number) => value.toLocaleString();
const getDurationLabel = (start: string, end: string) => {
const startMs = new Date(start).getTime();
const endMs = new Date(end).getTime();
@ -17,8 +16,11 @@ const getDurationLabel = (start: string, end: string) => {
};
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 = [

View file

@ -1,5 +1,5 @@
import type React from "react";
import { formatBytes } from "~/utils/format-bytes";
import { useFormatBytes } from "~/client/hooks/use-format-bytes";
type ByteSizeProps = {
bytes: number;
@ -14,6 +14,7 @@ type ByteSizeProps = {
};
export function ByteSize(props: ByteSizeProps) {
const formatBytes = useFormatBytes();
const {
bytes,
base = 1000,

View file

@ -3,7 +3,7 @@ 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 { formatDateTime } from "~/client/lib/datetime";
import { useTimeFormat } from "~/client/lib/datetime";
import { cn } from "~/client/lib/utils";
interface CronInputProps {
@ -13,6 +13,7 @@ interface CronInputProps {
}
export function CronInput({ value, onChange, error }: CronInputProps) {
const { formatDateTime } = useTimeFormat();
const { isValid, nextRuns, parseError } = useMemo(() => {
if (!value) {
return { isValid: false, nextRuns: [], parseError: null };
@ -59,7 +60,7 @@ export function CronInput({ value, onChange, error }: CronInputProps) {
{value && (
<div>
{isValid ? (
<CheckCircle2 className="h-4 w-4 text-green-500" />
<CheckCircle2 className="h-4 w-4 text-success" />
) : (
<AlertCircle className="h-4 w-4 text-destructive" />
)}

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

@ -1,6 +1,6 @@
import { useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { useHotkey } from "@tanstack/react-hotkeys";
import { useHotkeySequence } from "@tanstack/react-hotkeys";
import { getDevPanelOptions } from "~/client/api-client/@tanstack/react-query.gen";
import { DevPanel } from "./dev-panel";
@ -10,7 +10,12 @@ export function DevPanelListener() {
...getDevPanelOptions(),
});
useHotkey("Mod+Shift+D", () => setIsOpen(true), { enabled: !!devPanelStatus?.enabled, preventDefault: true });
useHotkeySequence(["D", "E", "V"], () => setIsOpen(true), {
enabled: !!devPanelStatus?.enabled,
preventDefault: true,
ignoreInputs: true,
timeout: 1000,
});
if (!devPanelStatus?.enabled) {
return null;

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

@ -134,15 +134,10 @@ export const FileBrowser = (props: FileBrowserProps) => {
}
const bodyWithScroll = useScrollArea ? <ScrollArea className={scrollAreaClassName}>{body}</ScrollArea> : body;
const wrappedBody = treeContainerClassName ? (
<div className={treeContainerClassName}>{bodyWithScroll}</div>
) : (
bodyWithScroll
);
return (
<div className={className}>
{wrappedBody}
<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>

View file

@ -3,7 +3,8 @@ import { browseFilesystemOptions } from "~/client/api-client/@tanstack/react-que
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 "~/utils/path";
import { normalizeAbsolutePath } from "@zerobyte/core/utils";
import { logger } from "~/client/lib/logger";
type LocalFileBrowserProps = FileBrowserUiProps & {
initialPath?: string;
@ -26,17 +27,10 @@ export const LocalFileBrowser = ({ initialPath = "/", enabled = true, ...uiProps
return await queryClient.ensureQueryData(browseFilesystemOptions({ query: { path } }));
},
prefetchFolder: (path) => {
void queryClient.prefetchQuery(browseFilesystemOptions({ query: { path } }));
void queryClient.prefetchQuery(browseFilesystemOptions({ query: { path } })).catch((e) => logger.error(e));
},
});
const errorDetails = parseError(error)?.message;
const errorMessage = errorDetails
? `Failed to load directories: ${errorDetails}`
: error
? "Failed to load directories"
: undefined;
return (
<FileBrowser
{...uiProps}
@ -49,7 +43,7 @@ export const LocalFileBrowser = ({ initialPath = "/", enabled = true, ...uiProps
getFolderPagination={fileBrowser.getFolderPagination}
isLoading={fileBrowser.isLoading}
isEmpty={fileBrowser.isEmpty}
errorMessage={errorMessage}
errorMessage={parseError(error)?.message}
loadingMessage={uiProps.loadingMessage ?? "Loading directories..."}
emptyMessage={uiProps.emptyMessage ?? "No subdirectories found"}
/>

View file

@ -4,106 +4,107 @@ import { listSnapshotFilesOptions } from "~/client/api-client/@tanstack/react-qu
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 "~/utils/path";
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;
basePath?: string;
queryBasePath?: string;
displayBasePath?: string;
pageSize?: number;
enabled?: boolean;
onSingleSelectionKindChange?: (kind: "file" | "dir" | null) => void;
};
export const SnapshotTreeBrowser = ({
repositoryId,
snapshotId,
basePath = "/",
pageSize = 500,
enabled = true,
...uiProps
}: SnapshotTreeBrowserProps) => {
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 normalizedBasePath = normalizeAbsolutePath(basePath);
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: normalizedBasePath },
query: { path: normalizedQueryBasePath },
}),
enabled,
});
const stripBasePath = useCallback(
(path: string): string => {
if (normalizedBasePath === "/") return path;
if (path === normalizedBasePath) return "/";
if (path.startsWith(`${normalizedBasePath}/`)) {
return path.slice(normalizedBasePath.length);
}
return path;
},
[normalizedBasePath],
);
const addBasePath = useCallback(
(displayPath: string): string => {
if (normalizedBasePath === "/") return displayPath;
if (displayPath === "/") return normalizedBasePath;
return `${normalizedBasePath}${displayPath}`;
},
[normalizedBasePath],
);
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(stripBasePath(fullPath));
displayPaths.add(displayPathFns.strip(fullPath));
}
return displayPaths;
}, [selectedPaths, stripBasePath]);
}, [displayPathFns, selectedPaths]);
const fileBrowser = useFileBrowser({
initialData: data,
isLoading,
fetchFolder: async (path, offset = 0) => {
fetchFolder: async (displayPath, offset = 0) => {
return await queryClient.ensureQueryData(
listSnapshotFilesOptions({
path: { shortId: repositoryId, snapshotId },
query: {
path,
offset: offset.toString(),
limit: pageSize.toString(),
},
query: { path: displayPath, offset: offset, limit: pageSize },
}),
);
},
prefetchFolder: (path) => {
void queryClient.prefetchQuery(
listSnapshotFilesOptions({
path: { shortId: repositoryId, snapshotId },
query: {
path,
offset: "0",
limit: pageSize.toString(),
},
}),
);
},
pathTransform: {
strip: stripBasePath,
add: addBasePath,
},
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]);
@ -114,7 +115,7 @@ export const SnapshotTreeBrowser = ({
const nextFullPaths = new Set<string>();
for (const displayPath of nextDisplayPaths) {
nextFullPaths.add(addBasePath(displayPath));
nextFullPaths.add(displayPathFns.add(displayPath));
}
if (onSingleSelectionKindChange) {
@ -132,16 +133,9 @@ export const SnapshotTreeBrowser = ({
onSelectionChange(nextFullPaths);
},
[onSelectionChange, addBasePath, onSingleSelectionKindChange, displayPathKinds],
[displayPathFns, displayPathKinds, onSelectionChange, onSingleSelectionKindChange],
);
const errorDetails = parseError(error)?.message;
const errorMessage = errorDetails
? `Failed to load files: ${errorDetails}`
: error
? "Failed to load files"
: undefined;
return (
<FileBrowser
{...fileBrowserUiProps}
@ -154,7 +148,7 @@ export const SnapshotTreeBrowser = ({
getFolderPagination={fileBrowser.getFolderPagination}
isLoading={fileBrowser.isLoading}
isEmpty={fileBrowser.isEmpty}
errorMessage={errorMessage}
errorMessage={parseError(error)?.message}
loadingMessage={fileBrowserUiProps.loadingMessage ?? "Loading files..."}
selectedPaths={displaySelectedPaths}
onSelectionChange={onSelectionChange ? handleSelectionChange : undefined}

View file

@ -3,6 +3,7 @@ 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;
@ -24,27 +25,22 @@ export const VolumeFileBrowser = ({ volumeId, enabled = true, ...uiProps }: Volu
return await queryClient.ensureQueryData(
listFilesOptions({
path: { shortId: volumeId },
query: { path, offset: offset?.toString() },
query: { path, offset: offset },
}),
);
},
prefetchFolder: (path) => {
void queryClient.prefetchQuery(
listFilesOptions({
path: { shortId: volumeId },
query: { path },
}),
);
void queryClient
.prefetchQuery(
listFilesOptions({
path: { shortId: volumeId },
query: { path },
}),
)
.catch((e) => logger.error(e));
},
});
const errorDetails = parseError(error)?.message;
const errorMessage = errorDetails
? `Failed to load files: ${errorDetails}`
: error
? "Failed to load files"
: undefined;
return (
<FileBrowser
{...uiProps}
@ -57,7 +53,7 @@ export const VolumeFileBrowser = ({ volumeId, enabled = true, ...uiProps }: Volu
getFolderPagination={fileBrowser.getFolderPagination}
isLoading={fileBrowser.isLoading}
isEmpty={fileBrowser.isEmpty}
errorMessage={errorMessage}
errorMessage={parseError(error)?.message}
loadingMessage={uiProps.loadingMessage ?? "Loading files..."}
emptyMessage={uiProps.emptyMessage ?? "This volume appears to be empty."}
/>

View file

@ -114,20 +114,25 @@ 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
@ -581,8 +586,10 @@ const NodeButton = memo(({ depth, icon, onClick, onMouseEnter, className, childr
);
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}
@ -591,7 +598,7 @@ const NodeButton = memo(({ depth, icon, onClick, onMouseEnter, className, childr
>
{icon}
<div className="truncate w-full flex items-center gap-2">{children}</div>
</button>
</div>
);
});
@ -625,6 +632,10 @@ 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, {
@ -636,6 +647,33 @@ function buildFileList(files: FileEntry[], foldersOnly = false): Node[] {
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,
});
}
}
// Convert map to array and sort
@ -702,5 +740,7 @@ function compareNodes(a: Node, b: Node): number {
return a.kind === "folder" ? -1 : 1;
}
return a.name.localeCompare(b.name);
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",
"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,14 +1,16 @@
import { LifeBuoy } from "lucide-react";
import { LifeBuoy, LogOut } from "lucide-react";
import { toast } from "sonner";
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 { 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";
type Props = {
loaderData: AppContext;
@ -32,36 +34,51 @@ export function Layout({ loaderData }: Props) {
return (
<SidebarProvider defaultOpen={loaderData.sidebarOpen}>
<AppSidebar />
<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/50 shrink-0">
<div className="flex items-center justify-between py-3 sm:py-4 px-2 sm:px-8 mx-auto container gap-4">
<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.name}</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={handleLogout}>
Logout
</Button>
<Button variant="default" size="sm" className="relative overflow-hidden hidden lg:inline-flex">
<a
href="https://github.com/nicotsx/zerobyte/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>

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-green-200 bg-green-50 text-green-700 dark:border-green-500/40 dark:bg-green-500/10 dark:text-green-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

@ -59,7 +59,7 @@ export function OrganizationSwitcher() {
size="lg"
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
<div className="bg-black text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center overflow-hidden rounded-lg">
<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}
@ -74,7 +74,7 @@ export function OrganizationSwitcher() {
<span className="truncate font-medium">{activeOrganization?.name}</span>
<span className="truncate text-xs">{organizations.length} organizations</span>
</div>
<ChevronsUpDown className="ml-auto" />
<ChevronsUpDown className="ml-auto group-data-[collapsible=icon]:hidden" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent

View file

@ -2,7 +2,7 @@ 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 { formatDate } from "~/client/lib/datetime";
import { useTimeFormat } from "~/client/lib/datetime";
import type { UpdateInfoDto } from "~/server/modules/system/system.dto";
interface ReleaseNotesDialogProps {
@ -12,6 +12,8 @@ interface ReleaseNotesDialogProps {
}
export function ReleaseNotesDialog({ open, onOpenChange, updates }: ReleaseNotesDialogProps) {
const { formatDate } = useTimeFormat();
if (!updates) return null;
return (

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;

View file

@ -1,8 +1,9 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { useMutation } from "@tanstack/react-query";
import { ChevronDown, Download, FolderOpen, RotateCcw } from "lucide-react";
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";
@ -21,7 +22,8 @@ import { SnapshotTreeBrowser } from "~/client/components/file-browsers/snapshot-
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 "~/schemas/restic";
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";
@ -33,16 +35,29 @@ interface RestoreFormProps {
repository: Repository;
snapshotId: string;
returnPath: string;
basePath?: string;
queryBasePath?: string;
displayBasePath?: string;
hasNonPosixSnapshotPaths?: boolean;
}
export function RestoreForm({ repository, snapshotId, returnPath, basePath }: RestoreFormProps) {
export function RestoreForm({
repository,
snapshotId,
returnPath,
queryBasePath,
displayBasePath,
hasNonPosixSnapshotPaths = false,
}: RestoreFormProps) {
const navigate = useNavigate();
const { addEventListener } = useServerEvents();
const volumeBasePath = basePath ?? "/";
const snapshotBasePath = queryBasePath ?? "/";
const hasMismatchedDisplayBasePath = displayBasePath && !isPathWithin(displayBasePath, snapshotBasePath);
const restoreRequiresCustomTarget = hasNonPosixSnapshotPaths || hasMismatchedDisplayBasePath;
const [restoreLocation, setRestoreLocation] = useState<RestoreLocation>("original");
const [restoreLocation, setRestoreLocation] = useState<RestoreLocation>(
restoreRequiresCustomTarget ? "custom" : "original",
);
const [customTargetPath, setCustomTargetPath] = useState("");
const [overwriteMode, setOverwriteMode] = useState<OverwriteMode>("always");
const [showAdvanced, setShowAdvanced] = useState(false);
@ -54,6 +69,15 @@ export function RestoreForm({ repository, snapshotId, returnPath, basePath }: Re
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();
@ -113,13 +137,13 @@ export function RestoreForm({ repository, snapshotId, returnPath, basePath }: Re
});
const handleRestore = useCallback(() => {
const excludeXattrArray = excludeXattr
?.split(",")
.map((s) => s.trim())
const excludeXattrValues = excludeXattr
.split(",")
.map((value) => value.trim())
.filter(Boolean);
const isCustomLocation = restoreLocation === "custom";
const targetPath = isCustomLocation && customTargetPath.trim() ? customTargetPath.trim() : undefined;
const targetPath = isCustomLocation && hasCustomTargetPath ? trimmedCustomTargetPath : undefined;
const includePaths = Array.from(selectedPaths);
@ -133,7 +157,8 @@ export function RestoreForm({ repository, snapshotId, returnPath, basePath }: Re
body: {
snapshotId,
include: includePaths.length > 0 ? includePaths : undefined,
excludeXattr: excludeXattrArray && excludeXattrArray.length > 0 ? excludeXattrArray : undefined,
selectedItemKind: includePaths.length === 1 ? (selectedPathKind ?? undefined) : undefined,
excludeXattr: excludeXattrValues.length > 0 ? excludeXattrValues : undefined,
targetPath,
overwrite: overwriteMode,
},
@ -142,38 +167,32 @@ export function RestoreForm({ repository, snapshotId, returnPath, basePath }: Re
repository.shortId,
snapshotId,
excludeXattr,
hasCustomTargetPath,
restoreLocation,
customTargetPath,
trimmedCustomTargetPath,
selectedPaths,
selectedPathKind,
overwriteMode,
restoreSnapshot,
]);
const handleDownload = useCallback(() => {
if (selectedPaths.size > 1) {
return;
}
if (selectedPaths.size > 1) return;
const dumpUrl = new URL(
const url = new URL(
`/api/v1/repositories/${repository.shortId}/snapshots/${snapshotId}/dump`,
window.location.origin,
);
if (selectedPaths.size === 1) {
const [selectedPath] = selectedPaths;
if (selectedPath) {
dumpUrl.searchParams.set("path", selectedPath);
if (selectedPathKind) {
dumpUrl.searchParams.set("kind", selectedPathKind);
}
const [selectedPath] = selectedPaths;
if (selectedPath) {
url.searchParams.set("path", selectedPath);
if (selectedPathKind) {
url.searchParams.set("kind", selectedPathKind);
}
}
const link = document.createElement("a");
link.href = dumpUrl.toString();
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.location.assign(url.toString());
}, [repository.shortId, snapshotId, selectedPathKind, selectedPaths]);
const acknowledgeRestoreResult = useCallback(() => {
@ -187,13 +206,15 @@ export function RestoreForm({ repository, snapshotId, returnPath, basePath }: Re
}
}, []);
const canRestore = restoreLocation === "original" || customTargetPath.trim();
const canDownload = selectedPaths.size <= 1;
const canRestore = restoreRequiresCustomTarget
? hasCustomTargetPath
: restoreLocation === "original" || hasCustomTargetPath;
const canDownload = selectedPathCount <= 1;
const isRestoreRunning = isRestoring || isRestoreActive;
function getDownloadButtonText(): string {
if (selectedPaths.size > 0) {
return `Download ${selectedPaths.size} ${selectedPaths.size === 1 ? "item" : "items"}`;
if (selectedPathCount > 0) {
return `Download ${selectedPathCount} ${selectedPathCount === 1 ? "item" : "items"}`;
}
return "Download All";
}
@ -202,8 +223,8 @@ export function RestoreForm({ repository, snapshotId, returnPath, basePath }: Re
if (isRestoreRunning) {
return "Restoring...";
}
if (selectedPaths.size > 0) {
return `Restore ${selectedPaths.size} ${selectedPaths.size === 1 ? "item" : "items"}`;
if (selectedPathCount > 0) {
return `Restore ${selectedPathCount} ${selectedPathCount === 1 ? "item" : "items"}`;
}
return "Restore All";
}
@ -245,6 +266,18 @@ export function RestoreForm({ repository, snapshotId, returnPath, basePath }: Re
<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>
@ -258,6 +291,7 @@ export function RestoreForm({ repository, snapshotId, returnPath, basePath }: Re
size="sm"
className="flex justify-start gap-2"
onClick={() => setRestoreLocation("original")}
disabled={!!restoreRequiresCustomTarget}
>
<RotateCcw size={16} className="mr-1" />
Original location
@ -352,7 +386,8 @@ export function RestoreForm({ repository, snapshotId, returnPath, basePath }: Re
<SnapshotTreeBrowser
repositoryId={repository.shortId}
snapshotId={snapshotId}
basePath={volumeBasePath}
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"

View file

@ -1,9 +1,10 @@
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 { formatBytes } from "~/utils/format-bytes";
import { formatDuration } from "~/utils/utils";
type Props = {
@ -12,6 +13,8 @@ type Props = {
};
export const RestoreProgress = ({ repositoryId, snapshotId }: Props) => {
const formatBytes = useFormatBytes();
const { locale } = useRootLoaderData();
const { addEventListener } = useServerEvents();
const [progress, setProgress] = useState<RestoreProgressEvent | null>(null);
@ -71,9 +74,9 @@ export const RestoreProgress = ({ repositoryId, snapshotId }: Props) => {
<div>
<p className="text-xs uppercase text-muted-foreground">Files</p>
<p className="font-medium">
{progress.files_restored.toLocaleString()}
{progress.files_restored.toLocaleString(locale)}
&nbsp;/&nbsp;
{progress.total_files.toLocaleString()}
{progress.total_files.toLocaleString(locale)}
</p>
</div>
<div>

View file

@ -25,8 +25,8 @@ import {
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 { formatDateTime } from "~/client/lib/datetime";
import {
deleteSnapshotsMutation,
listSnapshotsQueryKey,
@ -49,6 +49,7 @@ type Props = {
export const SnapshotsTable = ({ snapshots, repositoryId, backups, listSnapshotsQueryOptions }: Props) => {
const queryClient = useQueryClient();
const navigate = useNavigate();
const { formatDateTime } = useTimeFormat();
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
const [showBulkDeleteConfirm, setShowBulkDeleteConfirm] = useState(false);

View file

@ -12,8 +12,8 @@ interface StatusDotProps {
export const StatusDot = ({ variant, label, animated }: StatusDotProps) => {
const statusMapping = {
success: {
color: "bg-green-500",
colorLight: "bg-emerald-400",
color: "bg-success shadow-[0_0_8px_rgba(16,185,129,0.5)]",
colorLight: "bg-success/60",
animated: animated ?? true,
},
neutral: {
@ -40,12 +40,12 @@ export const StatusDot = ({ variant, label, animated }: StatusDotProps) => {
return (
<Tooltip>
<TooltipTrigger>
<TooltipTrigger asChild aria-label={label}>
<span className="relative flex size-3 mx-auto">
{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}`,
)}
/>
@ -54,7 +54,7 @@ export const StatusDot = ({ variant, label, animated }: StatusDotProps) => {
</span>
</TooltipTrigger>
<TooltipContent>
<p>{label}</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

@ -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,3 +1,4 @@
// fallow-ignore-file unused-export
import type * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { ChevronRight, MoreHorizontal } from "lucide-react";
@ -12,17 +13,16 @@ function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
return (
<ol
data-slot="breadcrumb-list"
className={cn(
"text-muted-foreground flex items-center gap-1.5 text-sm sm:gap-2.5 min-w-0",
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 min-w-0", className)} {...props} />;
return (
<li data-slot="breadcrumb-item" className={cn("inline-flex items-center gap-1.5 min-w-0", className)} {...props} />
);
}
function BreadcrumbLink({

View file

@ -7,16 +7,16 @@ 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",
},

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-0.5 -top-0.5 h-0.5 w-4 bg-white/80" />
<span className="absolute -left-0.5 -top-0.5 h-4 w-0.5 bg-white/80" />
<span className="absolute -right-0.5 -top-0.5 h-0.5 w-4 bg-white/80" />
<span className="absolute -right-0.5 -top-0.5 h-4 w-0.5 bg-white/80" />
<span className="absolute -left-0.5 -bottom-0.5 h-0.5 w-4 bg-white/80" />
<span className="absolute -left-0.5 -bottom-0.5 h-4 w-0.5 bg-white/80" />
<span className="absolute -right-0.5 -bottom-0.5 h-0.5 w-4 bg-white/80" />
<span className="absolute -right-0.5 -bottom-0.5 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

@ -8,8 +8,8 @@ interface CodeBlockProps {
export const CodeBlock: React.FC<CodeBlockProps> = ({ code, filename }) => {
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" />
@ -18,7 +18,7 @@ export const CodeBlock: React.FC<CodeBlockProps> = ({ code, filename }) => {
</div>
</div>
<pre className="text-xs m-0 px-4 py-2 bg-card-header">
<code className="text-white/80 select-all">{code}</code>
<code className="text-foreground/80 select-all">{code}</code>
</pre>
</div>
);

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

@ -1,6 +1,7 @@
// fallow-ignore-file unused-export
import * as React from "react";
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { cn } from "~/client/lib/utils";

View file

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

View file

@ -1,6 +1,5 @@
import * as React from "react";
import { OTPInput, OTPInputContext } from "input-otp";
import { MinusIcon } from "lucide-react";
import { cn } from "~/client/lib/utils";
@ -55,12 +54,8 @@ function InputOTPSlot({
);
}
function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) {
return (
<div data-slot="input-otp-separator" role="separator" {...props}>
<MinusIcon />
</div>
);
function InputOTPSeparator({ ...props }: React.ComponentProps<"hr">) {
return <hr data-slot="input-otp-separator" {...props} />;
}
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };

View file

@ -9,6 +9,7 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
data-slot="input"
className={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 border bg-transparent px-3 py-1 text-sm shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
// "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground bg-black/5 dark:bg-white/5 flex h-9 w-full min-w-0 border-0 px-3 py-1 text-sm shadow-xs dark:shadow-[inset_0_1px_0_rgba(255,255,255,0.05)] transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className,

View file

@ -1,3 +1,4 @@
// fallow-ignore-file unused-export
import type * as React from "react";
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";

View file

@ -1,3 +1,4 @@
// fallow-ignore-file unused-export
import { Eye, EyeOff } from "lucide-react";
import type * as React from "react";
import { useMemo, useState } from "react";
@ -14,7 +15,7 @@ export const isStoredSecretValue = (value?: string): boolean => {
return value.startsWith("env://") || value.startsWith("file://") || value.startsWith("encv1:");
};
type SecretInputProps = Omit<React.ComponentProps<typeof Input>, "type">
type SecretInputProps = Omit<React.ComponentProps<typeof Input>, "type">;
export const SecretInput = ({ className, value, ...props }: SecretInputProps) => {
const [revealed, setRevealed] = useState(false);
@ -36,12 +37,7 @@ export const SecretInput = ({ className, value, ...props }: SecretInputProps) =>
return (
<div className="relative" data-secret-input>
<Input
{...props}
value={value}
type={type}
className={cn(!showAsPlaintext && "pr-10", className)}
/>
<Input {...props} value={value} type={type} className={cn(!showAsPlaintext && "pr-10", className)} />
{!showAsPlaintext && (
<Button
type="button"

View file

@ -1,19 +1,73 @@
import type * as React from "react";
// fallow-ignore-file unused-export
import * as React from "react";
import * as SelectPrimitive from "@radix-ui/react-select";
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
import { cn } from "~/client/lib/utils";
function Select({ ...props }: React.ComponentProps<typeof SelectPrimitive.Root>) {
return <SelectPrimitive.Root data-slot="select" {...props} />;
const SelectSsrValueContext = React.createContext<{
hydrated: boolean;
items: Map<string, React.ReactNode>;
value: string | undefined;
} | null>(null);
function collectSelectItems(children: React.ReactNode, items = new Map<string, React.ReactNode>()) {
for (const child of React.Children.toArray(children)) {
if (!React.isValidElement<{ children?: React.ReactNode; value?: string }>(child)) {
continue;
}
if ((child.type === SelectItem || child.type === SelectPrimitive.Item) && typeof child.props.value === "string") {
items.set(child.props.value, child.props.children);
}
if (child.props.children) {
collectSelectItems(child.props.children, items);
}
}
return items;
}
function Select({ children, value, ...props }: React.ComponentProps<typeof SelectPrimitive.Root>) {
const [hydrated, setHydrated] = React.useState(false);
const items = collectSelectItems(children);
React.useEffect(() => {
setHydrated(true);
}, []);
return (
<SelectSsrValueContext.Provider value={{ hydrated, items, value }}>
<SelectPrimitive.Root data-slot="select" value={value} {...props}>
{children}
</SelectPrimitive.Root>
</SelectSsrValueContext.Provider>
);
}
function SelectGroup({ ...props }: React.ComponentProps<typeof SelectPrimitive.Group>) {
return <SelectPrimitive.Group data-slot="select-group" {...props} />;
}
function SelectValue({ ...props }: React.ComponentProps<typeof SelectPrimitive.Value>) {
return <SelectPrimitive.Value data-slot="select-value" {...props} />;
function SelectValue({ children, placeholder, style, ...props }: React.ComponentProps<typeof SelectPrimitive.Value>) {
const context = React.useContext(SelectSsrValueContext);
const selectedItem =
context?.value !== undefined && context.value !== "" ? context.items.get(context.value) : undefined;
if (!context?.hydrated) {
return (
<span data-slot="select-value" {...props} style={{ pointerEvents: "none", ...style }}>
{children ?? selectedItem ?? placeholder}
</span>
);
}
return (
<SelectPrimitive.Value data-slot="select-value" placeholder={placeholder} style={style} {...props}>
{children}
</SelectPrimitive.Value>
);
}
function SelectTrigger({
@ -21,19 +75,35 @@ function SelectTrigger({
size = "default",
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
}: React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> & {
size?: "sm" | "default";
}) {
const context = React.useContext(SelectSsrValueContext);
const hasValue = context?.value !== undefined && context.value !== "";
const triggerClassName = cn(
"border-input data-placeholder:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&+select[aria-hidden=true]]:hidden [&:has(+select[aria-hidden=true]:last-child)]:mb-0",
className,
);
if (!context?.hydrated) {
return (
<div
id={props.id}
aria-disabled="true"
aria-invalid={props["aria-invalid"]}
data-placeholder={hasValue ? undefined : ""}
data-size={size}
data-slot="select-trigger"
className={triggerClassName}
>
{children}
<ChevronDownIcon className="size-4 opacity-50" />
</div>
);
}
return (
<SelectPrimitive.Trigger
data-slot="select-trigger"
data-size={size}
className={cn(
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
<SelectPrimitive.Trigger data-slot="select-trigger" data-size={size} className={triggerClassName} {...props}>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDownIcon className="size-4 opacity-50" />

View file

@ -1,3 +1,4 @@
// fallow-ignore-file unused-export
"use client";
import type * as React from "react";

View file

@ -1,3 +1,4 @@
// fallow-ignore-file unused-export
"use client";
import * as React from "react";
@ -344,7 +345,7 @@ function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
data-slot="sidebar-content"
data-sidebar="content"
className={cn(
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
"flex min-h-0 flex-1 flex-col gap-2 overflow-y-auto overflow-x-hidden group-data-[collapsible=icon]:overflow-hidden",
className,
)}
{...props}
@ -565,10 +566,16 @@ function SidebarMenuSkeleton({
}: React.ComponentProps<"div"> & {
showIcon?: boolean;
}) {
// Random width between 50 to 90%.
const skeletonId = React.useId();
const width = React.useMemo(() => {
return `${Math.floor(Math.random() * 40) + 50}%`;
}, []);
let hash = 0;
for (const char of skeletonId) {
hash = (hash * 31 + char.charCodeAt(0)) % 40;
}
return `${hash + 50}%`;
}, [skeletonId]);
return (
<div

View file

@ -1,8 +1,8 @@
import { useTheme } from "next-themes";
import { useTheme } from "~/client/components/theme-provider";
import { Toaster as Sonner, type ToasterProps } from "sonner";
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "dark" } = useTheme();
const { theme } = useTheme();
return (
<Sonner

View file

@ -1,9 +1,12 @@
import * as SwitchPrimitive from "@radix-ui/react-switch";
import type * as React from "react";
import * as React from "react";
import { useWebHaptics } from "web-haptics/react";
import { cn } from "~/client/lib/utils";
function Switch({ className, ...props }: React.ComponentProps<typeof SwitchPrimitive.Root>) {
const trigger = useWebHaptics().trigger;
return (
<SwitchPrimitive.Root
data-slot="switch"
@ -11,6 +14,10 @@ function Switch({ className, ...props }: React.ComponentProps<typeof SwitchPrimi
"cursor-pointer peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all 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}
>
<SwitchPrimitive.Thumb

View file

@ -1,3 +1,4 @@
// fallow-ignore-file unused-export
import type * as React from "react";
import { cn } from "~/client/lib/utils";

View file

@ -1,6 +1,7 @@
import * as TabsPrimitive from "@radix-ui/react-tabs";
import type * as React from "react";
import { useWebHaptics } from "web-haptics/react";
import { cn } from "~/client/lib/utils";
function Tabs({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Root>) {
@ -17,7 +18,14 @@ function TabsList({ className, ...props }: React.ComponentProps<typeof TabsPrimi
);
}
function TabsTrigger({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
function TabsTrigger({ className, onClick, ...props }: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
const { trigger } = useWebHaptics();
const handleClick: React.MouseEventHandler<HTMLButtonElement> = (event) => {
void trigger([{ duration: 8 }], { intensity: 0.3 });
onClick?.(event);
};
return (
<TabsPrimitive.Trigger
data-slot="tabs-trigger"
@ -29,24 +37,25 @@ function TabsTrigger({ className, ...props }: React.ComponentProps<typeof TabsPr
// Padding: 20px horizontal (8px for bracket tick + 12px gap to text)
"px-5",
// Transparent orange background for active state
"data-[state=active]:bg-[#FF453A]/10",
"data-[state=active]:bg-strong-accent/10",
// Left bracket - vertical line
"before:absolute before:left-0 before:top-0 before:h-7 before:w-0.5 before:bg-[#5D6570] before:transition-colors data-[state=active]:before:bg-[#FF453A]",
"before:absolute before:left-0 before:top-0 before:h-7 before:w-0.5 before:bg-muted-foreground/60 before:transition-colors data-[state=active]:before:bg-strong-accent",
// Left bracket - top tick
"after:absolute after:left-0 after:top-[-1px] after:w-2 after:h-0.5 after:bg-[#5D6570] after:transition-colors data-[state=active]:after:bg-[#FF453A]",
"after:absolute after:left-0 after:-top-px after:w-2 after:h-0.5 after:bg-muted-foreground/60 after:transition-colors data-[state=active]:after:bg-strong-accent",
className,
)}
onClick={handleClick}
{...props}
>
<span className="relative z-10">{props.children}</span>
{/* Left bracket - bottom tick */}
<span className="absolute left-0 bottom-[-1px] h-0.5 w-2 bg-[#5D6570] transition-colors group-data-[state=active]:bg-[#FF453A]" />
<span className="absolute left-0 bottom-[-1px] h-0.5 w-2 bg-muted-foreground/60 transition-colors group-data-[state=active]:bg-strong-accent" />
{/* Right bracket - top tick */}
<span className="absolute right-0 top-[-1px] h-0.5 w-2 bg-[#5D6570] transition-colors group-data-[state=active]:bg-[#FF453A]" />
<span className="absolute right-0 top-[-1px] h-0.5 w-2 bg-muted-foreground/60 transition-colors group-data-[state=active]:bg-strong-accent" />
{/* Right bracket - vertical line */}
<span className="absolute right-0 top-0 h-7 w-0.5 bg-[#5D6570] transition-colors group-data-[state=active]:bg-[#FF453A]" />
<span className="absolute right-0 top-0 h-7 w-0.5 bg-muted-foreground/60 transition-colors group-data-[state=active]:bg-strong-accent" />
{/* Right bracket - bottom tick */}
<span className="absolute right-0 bottom-[-1px] h-0.5 w-2 bg-[#5D6570] transition-colors group-data-[state=active]:bg-[#FF453A]" />
<span className="absolute right-0 bottom-[-1px] h-0.5 w-2 bg-muted-foreground/60 transition-colors group-data-[state=active]:bg-strong-accent" />
</TabsPrimitive.Trigger>
);
}

View file

@ -1,5 +1,5 @@
import { Cloud, Folder, Server } from "lucide-react";
import type { BackendType } from "~/schemas/volumes";
import type { BackendType } from "@zerobyte/contracts/volumes";
type VolumeIconProps = {
backend: BackendType;
@ -49,7 +49,7 @@ export const VolumeIcon = ({ backend }: VolumeIconProps) => {
const { icon: Icon, label } = getIconAndLabel(backend);
return (
<span className={`flex items-center gap-2 rounded-md px-2 py-1`}>
<span className={`flex items-center gap-2 rounded-md py-1`}>
<Icon className="h-4 w-4" />
{label}
</span>

View file

@ -0,0 +1,25 @@
import { CodeBlock } from "~/client/components/ui/code-block";
import { Label } from "~/client/components/ui/label";
type WebhookRequestPreviewProps = {
method: string;
url?: string;
contentType?: string;
headers?: string[];
body: string;
};
export const WebhookRequestPreview = ({ method, url, contentType, headers, body }: WebhookRequestPreviewProps) => {
const headerLines = [contentType ? `Content-Type: ${contentType}` : undefined, ...(headers ?? [])].filter(Boolean);
const previewCode = `${method} ${url || "https://api.example.com/webhook"}${headerLines.length > 0 ? `\n${headerLines.join("\n")}` : ""}
${body}`;
return (
<div className="space-y-2 pt-4 border-t">
<Label>Request Preview</Label>
<CodeBlock code={previewCode} filename="HTTP Request" />
<p className="text-[0.8rem] text-muted-foreground">This is a preview of the HTTP request that will be sent.</p>
</div>
);
};

View file

@ -0,0 +1,36 @@
import { describe, expect, test } from "vitest";
import { getIsSecureContextForRequest, isSecureContextOrigin } from "../is-secure-context";
describe("isSecureContextOrigin", () => {
test("treats HTTPS and loopback HTTP origins as secure contexts", () => {
expect(isSecureContextOrigin("https://example.com")).toBe(true);
expect(isSecureContextOrigin("http://localhost:3000")).toBe(true);
expect(isSecureContextOrigin("http://app.localhost")).toBe(true);
expect(isSecureContextOrigin("http://127.0.0.1:3000")).toBe(true);
expect(isSecureContextOrigin("http://[::1]:3000")).toBe(true);
});
test("treats non-local HTTP origins as insecure contexts", () => {
expect(isSecureContextOrigin("http://example.com")).toBe(false);
expect(isSecureContextOrigin("http://127.example.com")).toBe(false);
});
});
describe("getIsSecureContextForRequest", () => {
test("uses forwarded protocol and host when present", () => {
const request = new Request("http://internal.local/settings", {
headers: {
host: "internal.local",
"x-forwarded-host": "zerobyte.example.com",
"x-forwarded-proto": "https",
},
});
expect(getIsSecureContextForRequest(request)).toBe(true);
});
test("uses the request URL when forwarded headers are absent", () => {
expect(getIsSecureContextForRequest(new Request("http://zerobyte.example.com/settings"))).toBe(false);
expect(getIsSecureContextForRequest(new Request("https://zerobyte.example.com/settings"))).toBe(true);
});
});

View file

@ -1,6 +1,6 @@
import { createIsomorphicFn } from "@tanstack/react-start";
import { createServerFn } from "@tanstack/react-start";
import { config } from "~/server/core/config";
export const getOrigin = createIsomorphicFn()
.server(() => config.baseUrl)
.client(() => window.location.origin);
export const getOrigin = createServerFn().handler(() => {
return config.baseUrl;
});

View file

@ -0,0 +1,51 @@
import { createIsomorphicFn } from "@tanstack/react-start";
import { getRequest } from "@tanstack/react-start/server";
import ipaddr from "ipaddr.js";
const getFirstHeaderValue = (value: string | null) => value?.split(",")[0]?.trim();
const isLoopbackIp = (hostname: string) => {
try {
return ipaddr.parse(hostname).range() === "loopback";
} catch {
return false;
}
};
const getRequestOrigin = (request: Request) => {
const requestUrl = new URL(request.url);
const forwardedProto = getFirstHeaderValue(request.headers.get("x-forwarded-proto"));
const forwardedHost = getFirstHeaderValue(request.headers.get("x-forwarded-host"));
const host = forwardedHost || getFirstHeaderValue(request.headers.get("host"));
if (!host) {
return requestUrl.origin;
}
const protocol = (forwardedProto || requestUrl.protocol).replace(/:$/, "");
return `${protocol}://${host}`;
};
export const isSecureContextOrigin = (origin: string) => {
const url = new URL(origin);
if (url.protocol === "https:") {
return true;
}
const hostname = url.hostname.replace(/^\[(.*)\]$/, "$1").toLowerCase();
return hostname === "localhost" || hostname.endsWith(".localhost") || isLoopbackIp(hostname);
};
export const getIsSecureContextForRequest = (request: Request) => isSecureContextOrigin(getRequestOrigin(request));
export const getIsSecureContext = createIsomorphicFn()
.server(() => {
try {
return getIsSecureContextForRequest(getRequest());
} catch {
return true;
}
})
.client(() => window.isSecureContext ?? true);

View file

@ -0,0 +1,141 @@
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { useEffect, useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { cleanup, createTestQueryClient, render, screen } from "~/test/test-utils";
import { useServerEvents } from "../use-server-events";
class MockEventSource {
static instances: MockEventSource[] = [];
public onerror: ((event: Event) => void) | null = null;
public close = vi.fn(() => {});
private listeners = new Map<string, Set<(event: Event) => void>>();
constructor(public url: string) {
MockEventSource.instances.push(this);
}
addEventListener(type: string, listener: EventListenerOrEventListenerObject) {
const listeners = this.listeners.get(type) ?? new Set<(event: Event) => void>();
const callback = typeof listener === "function" ? listener : (event: Event) => listener.handleEvent(event);
listeners.add(callback);
this.listeners.set(type, listeners);
}
emit(type: string, data: unknown) {
const event = new MessageEvent(type, {
data: JSON.stringify(data),
});
for (const listener of this.listeners.get(type) ?? []) {
listener(event);
}
}
static reset() {
MockEventSource.instances = [];
}
}
const originalEventSource = globalThis.EventSource;
const originalConsoleInfo = console.info;
const originalConsoleError = console.error;
const ConnectionConsumer = ({ enabled = true }: { enabled?: boolean }) => {
useServerEvents({ enabled });
return null;
};
const BackupCompletedListener = ({ scheduleId }: { scheduleId: string }) => {
const { addEventListener } = useServerEvents();
const [status, setStatus] = useState("pending");
useEffect(() => {
const abortController = new AbortController();
addEventListener(
"backup:completed",
(event) => {
if (event.scheduleId === scheduleId) {
setStatus(event.status);
}
},
{ signal: abortController.signal },
);
return () => abortController.abort();
}, [addEventListener, scheduleId]);
return <div>{status}</div>;
};
const QueryStatusConsumer = ({ getValue }: { getValue: () => string }) => {
const { data } = useQuery({
queryKey: ["backup-status"],
queryFn: async () => getValue(),
});
return <div>{data}</div>;
};
describe("useServerEvents", () => {
beforeEach(() => {
MockEventSource.reset();
globalThis.EventSource = MockEventSource as unknown as typeof EventSource;
console.info = vi.fn(() => {});
console.error = vi.fn(() => {});
});
afterEach(() => {
cleanup();
globalThis.EventSource = originalEventSource;
console.info = originalConsoleInfo;
console.error = originalConsoleError;
MockEventSource.reset();
});
test("shares one EventSource across consumers and refreshes active queries on backup completion", async () => {
const queryClient = createTestQueryClient();
let queryValue = "before";
const view = render(
<>
<ConnectionConsumer />
<BackupCompletedListener scheduleId="0b9c940b" />
<QueryStatusConsumer getValue={() => queryValue} />
</>,
{ queryClient },
);
expect(MockEventSource.instances).toHaveLength(1);
expect(await screen.findByText("before")).toBeTruthy();
queryValue = "after";
MockEventSource.instances[0]?.emit("backup:completed", {
organizationId: "default-org",
scheduleId: "0b9c940b",
volumeName: "synology",
repositoryName: "swiss-backup",
status: "success",
});
expect(await screen.findByText("success")).toBeTruthy();
expect(await screen.findByText("after")).toBeTruthy();
view.unmount();
expect(MockEventSource.instances[0]?.close).toHaveBeenCalledTimes(1);
});
test("waits to subscribe until enabled", () => {
const queryClient = createTestQueryClient();
const view = render(<ConnectionConsumer enabled={false} />, { queryClient });
expect(MockEventSource.instances).toHaveLength(0);
view.rerender(<ConnectionConsumer />);
expect(MockEventSource.instances).toHaveLength(1);
expect(MockEventSource.instances[0]?.url).toBe("/api/v1/events");
});
});

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