fix: batch of webui fixes, update webui deps, apply formatter, and regen protos
Release Please / release-please (push) Waiting to run
Release Preview / call-reusable-release (push) Waiting to run
Test / test-nix (push) Waiting to run
Test / test-win (push) Waiting to run

This commit is contained in:
garethgeorge
2026-07-08 01:04:30 -07:00
parent 0e31b76f54
commit 03e69c98dd
28 changed files with 173 additions and 177 deletions
+2 -1
View File
@@ -16,7 +16,7 @@
pkgs = nixpkgs.legacyPackages.${system};
devShellPkgs = with pkgs; [
go goreleaser nodejs_20 pnpm_9
protobuf buf protoc-gen-go protoc-gen-go-grpc
protobuf buf protoc-gen-go protoc-gen-go-grpc protoc-gen-connect-go
gnumake git restic rclone zsh oh-my-posh
];
in
@@ -47,6 +47,7 @@
buf
protoc-gen-go
protoc-gen-go-grpc
protoc-gen-connect-go
# General build tools
gnumake
+4 -4
View File
@@ -1,6 +1,6 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.5.1
// - protoc-gen-go-grpc v1.6.1
// - protoc (unknown)
// source: v1/authentication.proto
@@ -77,10 +77,10 @@ type AuthenticationServer interface {
type UnimplementedAuthenticationServer struct{}
func (UnimplementedAuthenticationServer) Login(context.Context, *LoginRequest) (*LoginResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Login not implemented")
return nil, status.Error(codes.Unimplemented, "method Login not implemented")
}
func (UnimplementedAuthenticationServer) HashPassword(context.Context, *types.StringValue) (*types.StringValue, error) {
return nil, status.Errorf(codes.Unimplemented, "method HashPassword not implemented")
return nil, status.Error(codes.Unimplemented, "method HashPassword not implemented")
}
func (UnimplementedAuthenticationServer) mustEmbedUnimplementedAuthenticationServer() {}
func (UnimplementedAuthenticationServer) testEmbeddedByValue() {}
@@ -93,7 +93,7 @@ type UnsafeAuthenticationServer interface {
}
func RegisterAuthenticationServer(s grpc.ServiceRegistrar, srv AuthenticationServer) {
// If the following call pancis, it indicates UnimplementedAuthenticationServer was
// If the following call panics, it indicates UnimplementedAuthenticationServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
+6 -6
View File
@@ -913,7 +913,7 @@ func (x *RestoreSnapshotRequest) GetTarget() string {
type ListSnapshotFilesRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
RepoGuid string `protobuf:"bytes,1,opt,name=repo_guid,json=repoGuid,proto3" json:"repo_guid,omitempty"`
RepoId string `protobuf:"bytes,1,opt,name=repo_id,json=repoId,proto3" json:"repo_id,omitempty"`
SnapshotId string `protobuf:"bytes,2,opt,name=snapshot_id,json=snapshotId,proto3" json:"snapshot_id,omitempty"`
Path string `protobuf:"bytes,3,opt,name=path,proto3" json:"path,omitempty"`
unknownFields protoimpl.UnknownFields
@@ -950,9 +950,9 @@ func (*ListSnapshotFilesRequest) Descriptor() ([]byte, []int) {
return file_v1_service_proto_rawDescGZIP(), []int{14}
}
func (x *ListSnapshotFilesRequest) GetRepoGuid() string {
func (x *ListSnapshotFilesRequest) GetRepoId() string {
if x != nil {
return x.RepoGuid
return x.RepoId
}
return ""
}
@@ -2032,9 +2032,9 @@ const file_v1_service_proto_rawDesc = "" +
"\vsnapshot_id\x18\x02 \x01(\tR\n" +
"snapshotId\x12\x12\n" +
"\x04path\x18\x03 \x01(\tR\x04path\x12\x16\n" +
"\x06target\x18\x04 \x01(\tR\x06target\"l\n" +
"\x18ListSnapshotFilesRequest\x12\x1b\n" +
"\trepo_guid\x18\x01 \x01(\tR\brepoGuid\x12\x1f\n" +
"\x06target\x18\x04 \x01(\tR\x06target\"h\n" +
"\x18ListSnapshotFilesRequest\x12\x17\n" +
"\arepo_id\x18\x01 \x01(\tR\x06repoId\x12\x1f\n" +
"\vsnapshot_id\x18\x02 \x01(\tR\n" +
"snapshotId\x12\x12\n" +
"\x04path\x18\x03 \x01(\tR\x04path\"V\n" +
+24 -24
View File
@@ -1,6 +1,6 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.5.1
// - protoc-gen-go-grpc v1.6.1
// - protoc (unknown)
// source: v1/service.proto
@@ -382,70 +382,70 @@ type BackrestServer interface {
type UnimplementedBackrestServer struct{}
func (UnimplementedBackrestServer) GetConfig(context.Context, *emptypb.Empty) (*Config, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetConfig not implemented")
return nil, status.Error(codes.Unimplemented, "method GetConfig not implemented")
}
func (UnimplementedBackrestServer) SetConfig(context.Context, *Config) (*Config, error) {
return nil, status.Errorf(codes.Unimplemented, "method SetConfig not implemented")
return nil, status.Error(codes.Unimplemented, "method SetConfig not implemented")
}
func (UnimplementedBackrestServer) SetupSftp(context.Context, *SetupSftpRequest) (*SetupSftpResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method SetupSftp not implemented")
return nil, status.Error(codes.Unimplemented, "method SetupSftp not implemented")
}
func (UnimplementedBackrestServer) CheckRepoExists(context.Context, *CheckRepoExistsRequest) (*CheckRepoExistsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method CheckRepoExists not implemented")
return nil, status.Error(codes.Unimplemented, "method CheckRepoExists not implemented")
}
func (UnimplementedBackrestServer) AddRepo(context.Context, *AddRepoRequest) (*Config, error) {
return nil, status.Errorf(codes.Unimplemented, "method AddRepo not implemented")
return nil, status.Error(codes.Unimplemented, "method AddRepo not implemented")
}
func (UnimplementedBackrestServer) RemoveRepo(context.Context, *RemoveRepoRequest) (*Config, error) {
return nil, status.Errorf(codes.Unimplemented, "method RemoveRepo not implemented")
return nil, status.Error(codes.Unimplemented, "method RemoveRepo not implemented")
}
func (UnimplementedBackrestServer) GetOperationEvents(*emptypb.Empty, grpc.ServerStreamingServer[OperationEvent]) error {
return status.Errorf(codes.Unimplemented, "method GetOperationEvents not implemented")
return status.Error(codes.Unimplemented, "method GetOperationEvents not implemented")
}
func (UnimplementedBackrestServer) GetOperations(context.Context, *GetOperationsRequest) (*OperationList, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetOperations not implemented")
return nil, status.Error(codes.Unimplemented, "method GetOperations not implemented")
}
func (UnimplementedBackrestServer) ListSnapshots(context.Context, *ListSnapshotsRequest) (*ResticSnapshotList, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListSnapshots not implemented")
return nil, status.Error(codes.Unimplemented, "method ListSnapshots not implemented")
}
func (UnimplementedBackrestServer) ListSnapshotFiles(context.Context, *ListSnapshotFilesRequest) (*ListSnapshotFilesResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListSnapshotFiles not implemented")
return nil, status.Error(codes.Unimplemented, "method ListSnapshotFiles not implemented")
}
func (UnimplementedBackrestServer) Backup(context.Context, *BackupRequest) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method Backup not implemented")
return nil, status.Error(codes.Unimplemented, "method Backup not implemented")
}
func (UnimplementedBackrestServer) DoRepoTask(context.Context, *DoRepoTaskRequest) (*ScheduleTaskResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method DoRepoTask not implemented")
return nil, status.Error(codes.Unimplemented, "method DoRepoTask not implemented")
}
func (UnimplementedBackrestServer) Forget(context.Context, *ForgetRequest) (*ScheduleTaskResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Forget not implemented")
return nil, status.Error(codes.Unimplemented, "method Forget not implemented")
}
func (UnimplementedBackrestServer) Restore(context.Context, *RestoreSnapshotRequest) (*ScheduleTaskResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Restore not implemented")
return nil, status.Error(codes.Unimplemented, "method Restore not implemented")
}
func (UnimplementedBackrestServer) Cancel(context.Context, *CancelOperationRequest) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method Cancel not implemented")
return nil, status.Error(codes.Unimplemented, "method Cancel not implemented")
}
func (UnimplementedBackrestServer) GetLogs(*LogDataRequest, grpc.ServerStreamingServer[types.BytesValue]) error {
return status.Errorf(codes.Unimplemented, "method GetLogs not implemented")
return status.Error(codes.Unimplemented, "method GetLogs not implemented")
}
func (UnimplementedBackrestServer) RunCommand(context.Context, *RunCommandRequest) (*RunCommandResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method RunCommand not implemented")
return nil, status.Error(codes.Unimplemented, "method RunCommand not implemented")
}
func (UnimplementedBackrestServer) GetDownloadURL(context.Context, *GetDownloadURLRequest) (*types.StringValue, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetDownloadURL not implemented")
return nil, status.Error(codes.Unimplemented, "method GetDownloadURL not implemented")
}
func (UnimplementedBackrestServer) ClearHistory(context.Context, *ClearHistoryRequest) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method ClearHistory not implemented")
return nil, status.Error(codes.Unimplemented, "method ClearHistory not implemented")
}
func (UnimplementedBackrestServer) PathAutocomplete(context.Context, *types.StringValue) (*types.StringList, error) {
return nil, status.Errorf(codes.Unimplemented, "method PathAutocomplete not implemented")
return nil, status.Error(codes.Unimplemented, "method PathAutocomplete not implemented")
}
func (UnimplementedBackrestServer) GetSummaryDashboard(context.Context, *emptypb.Empty) (*SummaryDashboardResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetSummaryDashboard not implemented")
return nil, status.Error(codes.Unimplemented, "method GetSummaryDashboard not implemented")
}
func (UnimplementedBackrestServer) GeneratePairingToken(context.Context, *GeneratePairingTokenRequest) (*GeneratePairingTokenResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GeneratePairingToken not implemented")
return nil, status.Error(codes.Unimplemented, "method GeneratePairingToken not implemented")
}
func (UnimplementedBackrestServer) mustEmbedUnimplementedBackrestServer() {}
func (UnimplementedBackrestServer) testEmbeddedByValue() {}
@@ -458,7 +458,7 @@ type UnsafeBackrestServer interface {
}
func RegisterBackrestServer(s grpc.ServiceRegistrar, srv BackrestServer) {
// If the following call pancis, it indicates UnimplementedBackrestServer was
// If the following call panics, it indicates UnimplementedBackrestServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
+6 -6
View File
@@ -1,6 +1,6 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.5.1
// - protoc-gen-go-grpc v1.6.1
// - protoc (unknown)
// source: v1sync/syncservice.proto
@@ -72,7 +72,7 @@ type BackrestSyncServiceServer interface {
type UnimplementedBackrestSyncServiceServer struct{}
func (UnimplementedBackrestSyncServiceServer) Sync(grpc.BidiStreamingServer[SyncStreamItem, SyncStreamItem]) error {
return status.Errorf(codes.Unimplemented, "method Sync not implemented")
return status.Error(codes.Unimplemented, "method Sync not implemented")
}
func (UnimplementedBackrestSyncServiceServer) mustEmbedUnimplementedBackrestSyncServiceServer() {}
func (UnimplementedBackrestSyncServiceServer) testEmbeddedByValue() {}
@@ -85,7 +85,7 @@ type UnsafeBackrestSyncServiceServer interface {
}
func RegisterBackrestSyncServiceServer(s grpc.ServiceRegistrar, srv BackrestSyncServiceServer) {
// If the following call pancis, it indicates UnimplementedBackrestSyncServiceServer was
// If the following call panics, it indicates UnimplementedBackrestSyncServiceServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
@@ -195,10 +195,10 @@ type BackrestSyncStateServiceServer interface {
type UnimplementedBackrestSyncStateServiceServer struct{}
func (UnimplementedBackrestSyncStateServiceServer) GetPeerSyncStatesStream(*SyncStateStreamRequest, grpc.ServerStreamingServer[PeerState]) error {
return status.Errorf(codes.Unimplemented, "method GetPeerSyncStatesStream not implemented")
return status.Error(codes.Unimplemented, "method GetPeerSyncStatesStream not implemented")
}
func (UnimplementedBackrestSyncStateServiceServer) SetRemoteClientConfig(context.Context, *SetRemoteClientConfigRequest) (*SetRemoteClientConfigResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method SetRemoteClientConfig not implemented")
return nil, status.Error(codes.Unimplemented, "method SetRemoteClientConfig not implemented")
}
func (UnimplementedBackrestSyncStateServiceServer) mustEmbedUnimplementedBackrestSyncStateServiceServer() {
}
@@ -212,7 +212,7 @@ type UnsafeBackrestSyncStateServiceServer interface {
}
func RegisterBackrestSyncStateServiceServer(s grpc.ServiceRegistrar, srv BackrestSyncStateServiceServer) {
// If the following call pancis, it indicates UnimplementedBackrestSyncStateServiceServer was
// If the following call panics, it indicates UnimplementedBackrestSyncStateServiceServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
+1 -12
View File
@@ -347,18 +347,7 @@ func (s *BackrestHandler) ListSnapshots(ctx context.Context, req *connect.Reques
func (s *BackrestHandler) ListSnapshotFiles(ctx context.Context, req *connect.Request[v1.ListSnapshotFilesRequest]) (*connect.Response[v1.ListSnapshotFilesResponse], error) {
query := req.Msg
// Lookup the repo by GUID to handle reimports correctly.
cfg, err := s.config.Get()
if err != nil {
return nil, fmt.Errorf("failed to get config: %w", err)
}
repoCfg := config.FindRepoByGUID(cfg, query.RepoGuid)
if repoCfg == nil {
return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("repo not found: %q", query.RepoGuid))
}
// Get the orchestrator for the repo if its configuration is available.
repo, err := s.orchestrator.GetRepoOrchestrator(repoCfg.Id)
repo, err := s.orchestrator.GetRepoOrchestrator(query.RepoId)
if err != nil {
return nil, withLookupCode(fmt.Errorf("failed to get repo: %w", err))
}
+1 -1
View File
@@ -164,7 +164,7 @@ message RestoreSnapshotRequest {
}
message ListSnapshotFilesRequest {
string repo_guid = 1;
string repo_id = 1;
string snapshot_id = 2;
string path = 3;
}
+1 -1
View File
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// @generated by protoc-gen-es v2.10.0 with parameter "target=ts"
// @generated by protoc-gen-es v2.11.0 with parameter "target=ts"
// @generated from file google/api/annotations.proto (package google.api, syntax proto3)
/* eslint-disable */
+1 -1
View File
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// @generated by protoc-gen-es v2.10.0 with parameter "target=ts"
// @generated by protoc-gen-es v2.11.0 with parameter "target=ts"
// @generated from file google/api/http.proto (package google.api, syntax proto3)
/* eslint-disable */
+1 -1
View File
@@ -1,4 +1,4 @@
// @generated by protoc-gen-es v2.10.0 with parameter "target=ts"
// @generated by protoc-gen-es v2.11.0 with parameter "target=ts"
// @generated from file types/value.proto (package types, syntax proto3)
/* eslint-disable */
+1 -1
View File
@@ -1,4 +1,4 @@
// @generated by protoc-gen-es v2.10.0 with parameter "target=ts"
// @generated by protoc-gen-es v2.11.0 with parameter "target=ts"
// @generated from file v1/authentication.proto (package v1, syntax proto3)
/* eslint-disable */
+1 -1
View File
@@ -1,4 +1,4 @@
// @generated by protoc-gen-es v2.10.0 with parameter "target=ts"
// @generated by protoc-gen-es v2.11.0 with parameter "target=ts"
// @generated from file v1/config.proto (package v1, syntax proto3)
/* eslint-disable */
+1 -1
View File
@@ -1,4 +1,4 @@
// @generated by protoc-gen-es v2.10.0 with parameter "target=ts"
// @generated by protoc-gen-es v2.11.0 with parameter "target=ts"
// @generated from file v1/crypto.proto (package v1, syntax proto3)
/* eslint-disable */
+1 -1
View File
@@ -1,4 +1,4 @@
// @generated by protoc-gen-es v2.10.0 with parameter "target=ts"
// @generated by protoc-gen-es v2.11.0 with parameter "target=ts"
// @generated from file v1/operations.proto (package v1, syntax proto3)
/* eslint-disable */
+1 -1
View File
@@ -1,4 +1,4 @@
// @generated by protoc-gen-es v2.10.0 with parameter "target=ts"
// @generated by protoc-gen-es v2.11.0 with parameter "target=ts"
// @generated from file v1/restic.proto (package v1, syntax proto3)
/* eslint-disable */
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,4 +1,4 @@
// @generated by protoc-gen-es v2.10.0 with parameter "target=ts"
// @generated by protoc-gen-es v2.11.0 with parameter "target=ts"
// @generated from file v1sync/syncservice.proto (package v1sync, syntax proto3)
/* eslint-disable */
-1
View File
@@ -36,7 +36,6 @@
"react-icons": "^5.5.0",
"react-js-cron": "^5.2.0",
"react-router": "^8.1.0",
"react-router-dom": "^7.18.1",
"react-virtualized": "^9.22.6",
"recharts": "^3.9.0"
},
+28 -46
View File
@@ -68,9 +68,6 @@ importers:
react-router:
specifier: ^8.1.0
version: 8.1.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
react-router-dom:
specifier: ^7.18.1
version: 7.18.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
react-virtualized:
specifier: ^9.22.6
version: 9.22.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
@@ -731,36 +728,42 @@ packages:
engines: {node: '>= 10.0.0'}
cpu: [arm]
os: [linux]
libc: [glibc]
'@parcel/watcher-linux-arm-musl@2.5.6':
resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==}
engines: {node: '>= 10.0.0'}
cpu: [arm]
os: [linux]
libc: [musl]
'@parcel/watcher-linux-arm64-glibc@2.5.6':
resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@parcel/watcher-linux-arm64-musl@2.5.6':
resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@parcel/watcher-linux-x64-glibc@2.5.6':
resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@parcel/watcher-linux-x64-musl@2.5.6':
resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [linux]
libc: [musl]
'@parcel/watcher-win32-arm64@2.5.6':
resolution: {integrity: sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==}
@@ -885,36 +888,42 @@ packages:
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-arm64-musl@1.1.3':
resolution: {integrity: sha512-BO9+oPL8K9poZJBfYPsXNtYjPE5uM3qeehT3aFcW4LITOl+iSqhp0abzjR2nWBUNjIZeKXjAEWBZ64WjNoHd6w==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rolldown/binding-linux-ppc64-gnu@1.1.3':
resolution: {integrity: sha512-f3VpLB1vQ0Eo6ecr/6cekLnvYMFF4YBFoVGkfkvPLq1bAkbAwHYQPZKoAmG6OJyTcxxoC+AvezGx/S1obNC0Mw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-s390x-gnu@1.1.3':
resolution: {integrity: sha512-AmurZ26Pqx/RI9N1gzEOCklkKXl927yjfXWUUS0O7Puh8ARM/Ob8qfrD3qnWksScdw6cSrW5PSHE9DyLu7+PtA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-x64-gnu@1.1.3':
resolution: {integrity: sha512-JJpqs8bRGITDOdbkNKnlojzBabbOHrqjSvDr0IVsZObE1lBcPjxItUEY9eWIDbxaJ3cGrXPWGfGkIxFijg/URg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-x64-musl@1.1.3':
resolution: {integrity: sha512-rSJcdjPxzA/by/6/rYs+v+bXU7UjvnbUWz8MJb6kh6+knqB1dCrtHg0uu7C/4haqJvqdkYHQ5IGn+tCH9GLW/g==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@rolldown/binding-openharmony-arm64@1.1.3':
resolution: {integrity: sha512-hQ3/PYkDJICgevvyNcVrihVeqq7k1Pp3VZ9lY+dauAYUJKO+auqApvANhvR1An9BhmqYKvW2Mu1F9u4DXSMLxQ==}
@@ -976,56 +985,67 @@ packages:
resolution: {integrity: sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.54.0':
resolution: {integrity: sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==}
cpu: [arm]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.54.0':
resolution: {integrity: sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.54.0':
resolution: {integrity: sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-loong64-gnu@4.54.0':
resolution: {integrity: sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==}
cpu: [loong64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-ppc64-gnu@4.54.0':
resolution: {integrity: sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-gnu@4.54.0':
resolution: {integrity: sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-musl@4.54.0':
resolution: {integrity: sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-s390x-gnu@4.54.0':
resolution: {integrity: sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.54.0':
resolution: {integrity: sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.54.0':
resolution: {integrity: sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==}
cpu: [x64]
os: [linux]
libc: [musl]
'@rollup/rollup-openharmony-arm64@4.54.0':
resolution: {integrity: sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==}
@@ -1519,10 +1539,6 @@ packages:
cookie-es@3.1.1:
resolution: {integrity: sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==}
cookie@1.1.1:
resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==}
engines: {node: '>=18'}
copy-to-clipboard@3.3.3:
resolution: {integrity: sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==}
@@ -1906,24 +1922,28 @@ packages:
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
lightningcss-linux-arm64-musl@1.32.0:
resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
lightningcss-linux-x64-gnu@1.32.0:
resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
lightningcss-linux-x64-musl@1.32.0:
resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [musl]
lightningcss-win32-arm64-msvc@1.32.0:
resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==}
@@ -2360,23 +2380,6 @@ packages:
redux:
optional: true
react-router-dom@7.18.1:
resolution: {integrity: sha512-KaZh+X/6UtEp28x51AUYZDMg9NGoz2ja3dNHa+ta/tk40vCzKhQ/RypCWBMLbmDr6//E24Vv5uPsrqXFozdkAg==}
engines: {node: '>=20.0.0'}
peerDependencies:
react: '>=18'
react-dom: '>=18'
react-router@7.18.1:
resolution: {integrity: sha512-GDLgg3i3uM0aeJO3Fm+TCS+sDQ7gu12T6x0qdTEzcwqEfleci7JwugVNIF3U//0FWKnJT7ptG+20B2jfDqnZAg==}
engines: {node: '>=20.0.0'}
peerDependencies:
react: '>=18'
react-dom: '>=18'
peerDependenciesMeta:
react-dom:
optional: true
react-router@8.1.0:
resolution: {integrity: sha512-Mdfi61uObuvWNN9OhChOC0HV6YWOIfKRzEWOvCHRSuQg8IM+Nv10edaM/2HE8ZixBpUTdQbruyWqC3sDkkh9vw==}
engines: {node: '>=22.22.0'}
@@ -2483,9 +2486,6 @@ packages:
engines: {node: '>=10'}
hasBin: true
set-cookie-parser@2.7.2:
resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==}
sha.js@2.4.11:
resolution: {integrity: sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==}
hasBin: true
@@ -4402,7 +4402,7 @@ snapshots:
babel-plugin-macros@3.1.0:
dependencies:
'@babel/runtime': 7.27.6
'@babel/runtime': 7.29.7
cosmiconfig: 7.1.0
resolve: 1.22.11
@@ -4479,8 +4479,6 @@ snapshots:
cookie-es@3.1.1: {}
cookie@1.1.1: {}
copy-to-clipboard@3.3.3:
dependencies:
toggle-selection: 1.0.6
@@ -5324,20 +5322,6 @@ snapshots:
'@types/react': 19.2.17
redux: 5.0.1
react-router-dom@7.18.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
dependencies:
react: 19.2.7
react-dom: 19.2.7(react@19.2.7)
react-router: 7.18.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
react-router@7.18.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
dependencies:
cookie: 1.1.1
react: 19.2.7
set-cookie-parser: 2.7.2
optionalDependencies:
react-dom: 19.2.7(react@19.2.7)
react-router@8.1.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
dependencies:
cookie-es: 3.1.1
@@ -5483,8 +5467,6 @@ snapshots:
semver@7.7.4: {}
set-cookie-parser@2.7.2: {}
sha.js@2.4.11:
dependencies:
inherits: 2.0.4
+39 -47
View File
@@ -1,4 +1,10 @@
import React, { Suspense, useCallback, useEffect, useMemo, useState } from "react";
import React, {
Suspense,
useCallback,
useEffect,
useMemo,
useState,
} from "react";
import {
FiCalendar,
FiDatabase,
@@ -66,12 +72,16 @@ import {
Routes,
useNavigate,
useParams,
Link as RouterLink,
useLocation,
} from "react-router-dom";
} from "react-router";
import { MainContentAreaTemplate } from "../components/layout/MainContentArea";
import { create } from "@bufbuild/protobuf";
import { PeerState, PlanMetadata, RepoMetadata, SetRemoteClientConfigRequestSchema } from "../../gen/ts/v1sync/syncservice_pb";
import {
PeerState,
PlanMetadata,
RepoMetadata,
SetRemoteClientConfigRequestSchema,
} from "../../gen/ts/v1sync/syncservice_pb";
import { useSyncStates } from "../state/peerStates";
import * as m from "../paraglide/messages";
import { Link } from "../components/ui/link";
@@ -145,7 +155,11 @@ const RepoViewContainer = () => {
borderColor="blue.200"
fontSize="sm"
color="blue.800"
_dark={{ bg: "blue.950", borderColor: "blue.800", color: "blue.200" }}
_dark={{
bg: "blue.950",
borderColor: "blue.800",
color: "blue.200",
}}
>
This is a remote repo from{" "}
<strong>{repo.originInstanceId}</strong>. Operation history only
@@ -293,11 +307,7 @@ const PeerNavItem = ({
{name}
</Text>
{onEdit && (
<Box
opacity={0}
_groupHover={{ opacity: 1 }}
transition="opacity 0.2s"
>
<Box opacity={0} _groupHover={{ opacity: 1 }} transition="opacity 0.2s">
<IconButton
size="xs"
variant="ghost"
@@ -471,20 +481,12 @@ const SidebarPlanItem = React.memo(
onClick={() => onNav(planPath)}
userSelect="none"
>
<Text
overflow="hidden"
textOverflow="ellipsis"
whiteSpace="nowrap"
>
<Text overflow="hidden" textOverflow="ellipsis" whiteSpace="nowrap">
{plan.id}
</Text>
</Box>
</Tooltip>
<Box
opacity={0}
_groupHover={{ opacity: 1 }}
transition="opacity 0.2s"
>
<Box opacity={0} _groupHover={{ opacity: 1 }} transition="opacity 0.2s">
<IconButton
size="xs"
variant="ghost"
@@ -545,11 +547,7 @@ const SidebarRepoItem = React.memo(
onClick={() => onNav(repoPath)}
userSelect="none"
>
<Text
overflow="hidden"
textOverflow="ellipsis"
whiteSpace="nowrap"
>
<Text overflow="hidden" textOverflow="ellipsis" whiteSpace="nowrap">
{repo.id}
</Text>
{repo.originInstanceId && (
@@ -565,11 +563,7 @@ const SidebarRepoItem = React.memo(
)}
</Box>
</Tooltip>
<Box
opacity={0}
_groupHover={{ opacity: 1 }}
transition="opacity 0.2s"
>
<Box opacity={0} _groupHover={{ opacity: 1 }} transition="opacity 0.2s">
<IconButton
size="xs"
variant="ghost"
@@ -600,18 +594,17 @@ const SidebarContent = ({ onClose }: { onClose?: () => void }) => {
const isActive = (path: string) => location.pathname === path;
const reposById = useMemo(() => config ? keyBy(config.repos, (r) => r.id) : {}, [config?.repos]);
const reposById = useMemo(
() => (config ? keyBy(config.repos, (r) => r.id) : {}),
[config?.repos],
);
// Replicate getSidenavItems functionality with Chakra components
if (!config) return null;
const configPlans = config.plans || [];
const localRepos = (config.repos || []).filter(
(r) => !r.originInstanceId,
);
const remoteRepos = (config.repos || []).filter(
(r) => !!r.originInstanceId,
);
const localRepos = (config.repos || []).filter((r) => !r.originInstanceId);
const remoteRepos = (config.repos || []).filter((r) => !!r.originInstanceId);
return (
<Box
@@ -1060,7 +1053,9 @@ const AuthenticationBoundary = ({
const timeoutPromise = new Promise<never>((_, reject) =>
setTimeout(
() =>
reject(new Error("Request timed out, backend may be unavailable")),
reject(
new Error("Request timed out, backend may be unavailable"),
),
10000,
),
);
@@ -1070,9 +1065,8 @@ const AuthenticationBoundary = ({
])) as Config;
setConfig(config);
if (shouldShowSettings(config)) {
const { SettingsModal } = await import(
"../features/settings/SettingsModal"
);
const { SettingsModal } =
await import("../features/settings/SettingsModal");
showModal(<SettingsModal />);
} else {
showModal(null);
@@ -1134,12 +1128,10 @@ const AuthenticationBoundary = ({
return <>{children}</>;
};
const IconForResource = React.memo(
({ selector }: { selector: OpSelector }) => {
const status = useResourceStatus(selector);
return iconForStatus(status);
},
);
const IconForResource = React.memo(({ selector }: { selector: OpSelector }) => {
const status = useResourceStatus(selector);
return iconForStatus(status);
});
const iconForStatus = (status: OperationStatus) => {
const color = colorForStatus(status);
+1 -1
View File
@@ -6,7 +6,7 @@ import { Toaster } from "../components/ui/toaster";
import "react-js-cron/dist/styles.css";
import { HashRouter } from "react-router-dom";
import { HashRouter } from "react-router";
import { AppProvider } from "./provider";
const Root = ({ children }: { children: React.ReactNode }) => {
+19 -1
View File
@@ -2,7 +2,7 @@ import { ChakraProvider, defaultSystem } from "@chakra-ui/react";
import { ThemeProvider } from "next-themes";
import React, { useContext, useEffect, useRef, useState } from "react";
import { useUserPreferences } from "../lib/userPreferences";
import { Config } from "../../gen/ts/v1/config_pb";
import { Config, Repo } from "../../gen/ts/v1/config_pb";
import { backrestService } from "../api/client";
// Config Context Logic
@@ -14,6 +14,24 @@ export const useConfig = (): ConfigCtx => {
return context;
};
// Lookup of repos by their stable GUID, derived from the config. Cached on the
// config object's identity so it is built once per config instance and shared
// across all consumers (and garbage-collected when the config is replaced),
// giving an O(1) lookup instead of rescanning config.repos on every render.
const EMPTY_REPOS_BY_GUID: Map<string, Repo> = new Map();
const reposByGuidCache = new WeakMap<Config, Map<string, Repo>>();
export const useReposByGuid = (): Map<string, Repo> => {
const [config] = useConfig();
if (!config) return EMPTY_REPOS_BY_GUID;
let byGuid = reposByGuidCache.get(config);
if (!byGuid) {
byGuid = new Map(config.repos.map((r) => [r.guid, r]));
reposByGuidCache.set(config, byGuid);
}
return byGuid;
};
export function AppProvider(props: { children: React.ReactNode }) {
useUserPreferences(); // Ensure locale is synced on startup
const [config, setConfig] = useState<Config | null>(null);
+1 -1
View File
@@ -31,7 +31,7 @@ export const InputGroup = React.forwardRef<HTMLDivElement, InputGroupProps>(
{React.cloneElement(children, {
...(startElement && { ps: "calc(var(--input-height) - 6px)" }),
...(endElement && { pe: "calc(var(--input-height) - 6px)" }),
...children.props,
...(children.props as Record<string, unknown>),
})}
{endElement && (
<InputElement placement="end" {...endElementProps}>
+2
View File
@@ -3,6 +3,8 @@ declare module "*.svg" {
export default content;
}
declare module "*.css";
declare module "*/paraglide/messages" {
const messages: any;
export = messages;
@@ -64,7 +64,8 @@ function agoText(ms: number): string {
const s = Math.floor((Date.now() - ms) / 1000);
if (s < 45) return m.dashboard_time_just_now();
if (s < 90) return m.dashboard_time_a_minute_ago();
if (s < 3600) return m.dashboard_time_minutes_ago({ count: Math.floor(s / 60) });
if (s < 3600)
return m.dashboard_time_minutes_ago({ count: Math.floor(s / 60) });
if (s < 5400) return m.dashboard_time_an_hour_ago();
if (s < 86400) {
const h = Math.floor(s / 3600);
@@ -82,7 +83,9 @@ function untilText(ms: number): string | null {
const s = Math.floor((ms - Date.now()) / 1000);
if (s <= 0) return m.dashboard_time_due_now();
if (s < 5400)
return m.dashboard_time_in_minutes({ count: Math.max(1, Math.round(s / 60)) });
return m.dashboard_time_in_minutes({
count: Math.max(1, Math.round(s / 60)),
});
if (s < 172800)
return m.dashboard_time_in_hours({ count: Math.round(s / 3600) });
return m.dashboard_time_in_days({ count: Math.round(s / 86400) });
@@ -181,7 +184,9 @@ interface SummaryStatus {
color: string;
}
function summaryStatus(summary: SummaryDashboardResponse_Summary): SummaryStatus {
function summaryStatus(
summary: SummaryDashboardResponse_Summary,
): SummaryStatus {
const rb = summary.recentBackups;
const latestStatus = rb?.status[0];
const latestTs = Number(rb?.timestampMs[0] ?? 0);
@@ -326,7 +331,7 @@ function progressFromOp(op: Operation): LiveProgress | null {
const useLiveProgress = (
planId: string,
running: boolean,
onFinished: () => void,
onFinished?: () => void,
) => {
const [progress, setProgress] = useState<LiveProgress | null>(null);
@@ -350,7 +355,7 @@ const useLiveProgress = (
if (finished) return;
finished = true;
setProgress(null);
onFinished();
onFinished?.();
return;
}
const p = progressFromOp(op);
@@ -942,7 +947,7 @@ const MultihostSummary = ({
return map;
}, [config?.repos]);
const knownHostTiles: JSX.Element[] = [];
const knownHostTiles: React.JSX.Element[] = [];
for (const cfgPeer of multihostConfig?.knownHosts ?? []) {
const peerState = peerStates.get(cfgPeer.keyid);
if (!peerState) continue;
@@ -955,7 +960,7 @@ const MultihostSummary = ({
);
}
const authorizedClientTiles: JSX.Element[] = [];
const authorizedClientTiles: React.JSX.Element[] = [];
for (const cfgPeer of multihostConfig?.authorizedClients ?? []) {
const peerState = peerStates.get(cfgPeer.keyid);
if (!peerState) continue;
+10 -2
View File
@@ -59,6 +59,7 @@ import { OperationIcon } from "./OperationIcon";
import { LogView } from "../../components/common/LogView";
import { create } from "@bufbuild/protobuf";
import { useReposByGuid } from "../../app/provider";
import { OperationListView } from "./OperationListView";
import * as m from "../../paraglide/messages";
import { FormModal } from "../../components/common/FormModal";
@@ -107,9 +108,17 @@ export const OperationRow = ({
showDelete?: boolean;
}>) => {
const showModal = useShowModal();
const reposByGuid = useReposByGuid();
const displayType = getTypeForDisplay(operation);
const setRefresh = useState(0)[1];
// The operation's repoId reflects the repo's name when the operation was
// recorded, which may be stale if the repo was removed and re-added under a
// different name. Resolve the current repoId from the stable repoGuid so that
// actions (e.g. browsing files, restoring) target the repo as it exists now.
const currentRepoId =
reposByGuid.get(operation.repoGuid)?.id ?? operation.repoId;
useEffect(() => {
if (operation.status === OperationStatus.STATUS_INPROGRESS) {
const interval = setInterval(() => {
@@ -293,8 +302,7 @@ export const OperationRow = ({
<SnapshotBrowser
snapshotId={snapshotOp.snapshot!.id}
snapshotOpId={operation.id}
repoId={operation.repoId}
repoGuid={operation.repoGuid}
repoId={currentRepoId}
planId={operation.planId}
/>
),
@@ -117,7 +117,6 @@ const findInTree = (
export const SnapshotBrowser = ({
repoId,
repoGuid,
planId,
snapshotId,
snapshotOpId,
@@ -125,7 +124,6 @@ export const SnapshotBrowser = ({
snapshotId: string;
snapshotOpId?: bigint;
repoId: string;
repoGuid: string;
planId?: string;
}>) => {
const showModal = useShowModal();
@@ -174,7 +172,7 @@ export const SnapshotBrowser = ({
// But we can trigger a load for root immediately to populate it if expanded.
loadData("/");
setExpandedKeys(new Set(["/"])); // Auto-expand root for better UX
}, [repoId, repoGuid, snapshotId]);
}, [repoId, snapshotId]);
const loadData = async (key: string) => {
// Check if node exists first
@@ -201,7 +199,7 @@ export const SnapshotBrowser = ({
const resp = await backrestService.listSnapshotFiles(
create(ListSnapshotFilesRequestSchema, {
path,
repoGuid,
repoId,
snapshotId,
}),
);
+2
View File
@@ -3,9 +3,11 @@
"target": "ESNext",
"useDefineForClassFields": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"types": ["node"],
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": false,
"ignoreDeprecations": "6.0",
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,