mirror of https://github.com/jetkvm/kvm.git
fix: remove localization from 0.4.10
This commit is contained in:
parent
5717761d16
commit
161c272099
11
go.mod
11
go.mod
|
|
@ -5,6 +5,7 @@ go 1.24.4
|
|||
require (
|
||||
github.com/Masterminds/semver/v3 v3.4.0
|
||||
github.com/beevik/ntp v1.4.3
|
||||
github.com/caarlos0/env/v11 v11.3.1
|
||||
github.com/coder/websocket v1.8.14
|
||||
github.com/coreos/go-oidc/v3 v3.15.0
|
||||
github.com/creack/pty v1.1.24
|
||||
|
|
@ -34,6 +35,8 @@ require (
|
|||
golang.org/x/crypto v0.42.0
|
||||
golang.org/x/net v0.44.0
|
||||
golang.org/x/sys v0.36.0
|
||||
google.golang.org/grpc v1.76.0
|
||||
google.golang.org/protobuf v1.36.10
|
||||
)
|
||||
|
||||
replace github.com/pojntfx/go-nbd v0.3.2 => github.com/chemhack/go-nbd v0.0.0-20241006125820-59e45f5b1e7b
|
||||
|
|
@ -42,14 +45,13 @@ require (
|
|||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bytedance/sonic v1.14.0 // indirect
|
||||
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
||||
github.com/caarlos0/env/v11 v11.3.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/creack/goselect v0.1.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.0 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.2 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.27.0 // indirect
|
||||
|
|
@ -82,7 +84,6 @@ require (
|
|||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||
github.com/vishvananda/netns v0.0.5 // indirect
|
||||
|
|
@ -90,11 +91,7 @@ require (
|
|||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||
golang.org/x/arch v0.20.0 // indirect
|
||||
golang.org/x/oauth2 v0.32.0 // indirect
|
||||
golang.org/x/sync v0.17.0 // indirect
|
||||
golang.org/x/text v0.30.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b // indirect
|
||||
google.golang.org/grpc v1.76.0 // indirect
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 // indirect
|
||||
google.golang.org/protobuf v1.36.10 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
|
|
|||
40
go.sum
40
go.sum
|
|
@ -46,8 +46,12 @@ github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
|
|||
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/go-co-op/gocron/v2 v2.16.6 h1:zI2Ya9sqvuLcgqJgV79LwoJXM8h20Z/drtB7ATbpRWo=
|
||||
github.com/go-co-op/gocron/v2 v2.16.6/go.mod h1:zAfC/GFQ668qHxOVl/D68Jh5Ce7sDqX6TJnSQyRkRBc=
|
||||
github.com/go-jose/go-jose/v4 v4.1.0 h1:cYSYxd3pw5zd2FSXk2vGdn9igQU2PS8MuxrCOCl0FdY=
|
||||
github.com/go-jose/go-jose/v4 v4.1.0/go.mod h1:GG/vqmYm3Von2nYiB2vGTXzdoNKE5tix5tuc6iAd+sw=
|
||||
github.com/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI=
|
||||
github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
|
|
@ -59,6 +63,8 @@ github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAu
|
|||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
|
|
@ -160,8 +166,6 @@ github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
|||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||
github.com/sourcegraph/tf-dag v0.2.2-0.20250131204052-3e8ff1477b4f h1:VgoRCP1efSCEZIcF2THLQ46+pIBzzgNiaUBe9wEDwYU=
|
||||
github.com/sourcegraph/tf-dag v0.2.2-0.20250131204052-3e8ff1477b4f/go.mod h1:pzro7BGorij2WgrjEammtrkbo3+xldxo+KaGLGUiD+Q=
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
|
|
@ -185,6 +189,18 @@ github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
|
|||
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
||||
go.bug.st/serial v1.6.4 h1:7FmqNPgVp3pu2Jz5PoPtbZ9jJO5gnEnZIvnI1lzve8A=
|
||||
go.bug.st/serial v1.6.4/go.mod h1:nofMJxTeNVny/m6+KaafC6vJGj3miwQZ6vW4BZUGJPI=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
|
||||
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
|
||||
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
|
||||
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
|
||||
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
|
||||
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
|
||||
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
|
||||
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
||||
|
|
@ -195,25 +211,25 @@ golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
|
|||
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
||||
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
|
||||
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||
golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
|
||||
golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
|
||||
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
|
||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
|
||||
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
|
||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b h1:zPKJod4w6F1+nRGDI9ubnXYhU9NSWoFAijkHkUXeTK8=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
|
||||
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 h1:F29+wU6Ee6qgu9TddPgooOdaqsxTMunOoj8KA5yuS5A=
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1/go.mod h1:5KF+wpkbTSbGcR9zteSqZV6fqFOWBl4Yde8En8MryZA=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
|
|
|||
11
hw.go
11
hw.go
|
|
@ -8,6 +8,8 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"os/exec"
|
||||
|
||||
"github.com/jetkvm/kvm/internal/ota"
|
||||
)
|
||||
|
||||
|
|
@ -30,14 +32,6 @@ func extractSerialNumber() (string, error) {
|
|||
return matches[1], nil
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
func readOtpEntropy() ([]byte, error) { //nolint:unused
|
||||
content, err := os.ReadFile("/sys/bus/nvmem/devices/rockchip-otp0/nvmem")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return content[0x17:0x1C], nil
|
||||
=======
|
||||
func hwReboot(force bool, postRebootAction *ota.PostRebootAction, delay time.Duration) error {
|
||||
logger.Info().Dur("delayMs", delay).Msg("reboot requested")
|
||||
|
||||
|
|
@ -69,7 +63,6 @@ func hwReboot(force bool, postRebootAction *ota.PostRebootAction, delay time.Dur
|
|||
}()
|
||||
|
||||
return nil
|
||||
>>>>>>> 752fb55 (refactor: OTA (#912))
|
||||
}
|
||||
|
||||
var deviceID string
|
||||
|
|
|
|||
|
|
@ -12,14 +12,12 @@ show_help() {
|
|||
echo
|
||||
echo "Optional:"
|
||||
echo " -u, --user <remote_user> Remote username (default: root)"
|
||||
echo " --gdb-port <port> GDB debug port (default: 2345)"
|
||||
echo " --run-go-tests Run go tests"
|
||||
echo " --run-go-tests-only Run go tests and exit"
|
||||
echo " --skip-ui-build Skip frontend/UI build"
|
||||
echo " --skip-native-build Skip native build"
|
||||
echo " --disable-docker Disable docker build"
|
||||
echo " --enable-sync-trace Enable sync trace (do not use in release builds)"
|
||||
echo " --native-binary Build and deploy the native binary (FOR DEBUGGING ONLY)"
|
||||
echo " -i, --install Build for release and install the app"
|
||||
echo " --help Display this help message"
|
||||
echo
|
||||
|
|
@ -28,6 +26,31 @@ show_help() {
|
|||
echo " $0 -r 192.168.0.17 -u admin"
|
||||
}
|
||||
|
||||
# Function to check if device is pingable
|
||||
check_ping() {
|
||||
local host=$1
|
||||
msg_info "▶ Checking if device is reachable at ${host}..."
|
||||
if ! ping -c 3 -W 5 "${host}" > /dev/null 2>&1; then
|
||||
msg_err "Error: Cannot reach device at ${host}"
|
||||
msg_err "Please verify the IP address and network connectivity"
|
||||
exit 1
|
||||
fi
|
||||
msg_info "✓ Device is reachable"
|
||||
}
|
||||
|
||||
# Function to check if SSH is accessible
|
||||
check_ssh() {
|
||||
local user=$1
|
||||
local host=$2
|
||||
msg_info "▶ Checking SSH connectivity to ${user}@${host}..."
|
||||
if ! ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o ConnectTimeout=10 "${user}@${host}" "echo 'SSH connection successful'" > /dev/null 2>&1; then
|
||||
msg_err "Error: Cannot establish SSH connection to ${user}@${host}"
|
||||
msg_err "Please verify SSH access and credentials"
|
||||
exit 1
|
||||
fi
|
||||
msg_info "✓ SSH connection successful"
|
||||
}
|
||||
|
||||
# Default values
|
||||
SCRIPT_PATH=$(realpath "$(dirname $(realpath "${BASH_SOURCE[0]}"))")
|
||||
REMOTE_USER="root"
|
||||
|
|
@ -35,8 +58,6 @@ REMOTE_PATH="/userdata/jetkvm/bin"
|
|||
SKIP_UI_BUILD=false
|
||||
SKIP_UI_BUILD_RELEASE=0
|
||||
SKIP_NATIVE_BUILD=0
|
||||
GDB_DEBUG_PORT=2345
|
||||
BUILD_NATIVE_BINARY=false
|
||||
ENABLE_SYNC_TRACE=0
|
||||
RESET_USB_HID_DEVICE=false
|
||||
LOG_TRACE_SCOPES="${LOG_TRACE_SCOPES:-jetkvm,cloud,websocket,native,jsonrpc}"
|
||||
|
|
@ -58,10 +79,6 @@ while [[ $# -gt 0 ]]; do
|
|||
REMOTE_USER="$2"
|
||||
shift 2
|
||||
;;
|
||||
--gdb-port)
|
||||
GDB_DEBUG_PORT="$2"
|
||||
shift 2
|
||||
;;
|
||||
--skip-ui-build)
|
||||
SKIP_UI_BUILD=true
|
||||
shift
|
||||
|
|
@ -74,6 +91,11 @@ while [[ $# -gt 0 ]]; do
|
|||
RESET_USB_HID_DEVICE=true
|
||||
shift
|
||||
;;
|
||||
--enable-sync-trace)
|
||||
ENABLE_SYNC_TRACE=1
|
||||
LOG_TRACE_SCOPES="${LOG_TRACE_SCOPES},synctrace"
|
||||
shift
|
||||
;;
|
||||
--disable-docker)
|
||||
BUILD_IN_DOCKER=false
|
||||
shift
|
||||
|
|
@ -91,10 +113,6 @@ while [[ $# -gt 0 ]]; do
|
|||
RUN_GO_TESTS=true
|
||||
shift
|
||||
;;
|
||||
--native-binary)
|
||||
BUILD_NATIVE_BINARY=true
|
||||
shift
|
||||
;;
|
||||
-i|--install)
|
||||
INSTALL_APP=true
|
||||
shift
|
||||
|
|
@ -123,10 +141,6 @@ fi
|
|||
# Check device connectivity before proceeding
|
||||
check_ping "${REMOTE_HOST}"
|
||||
check_ssh "${REMOTE_USER}" "${REMOTE_HOST}"
|
||||
function sshdev() {
|
||||
ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" "$@"
|
||||
return $?
|
||||
}
|
||||
|
||||
# check if the current CPU architecture is x86_64
|
||||
if [ "$(uname -m)" != "x86_64" ]; then
|
||||
|
|
@ -138,34 +152,6 @@ if [ "$BUILD_IN_DOCKER" = true ]; then
|
|||
build_docker_image
|
||||
fi
|
||||
|
||||
if [ "$BUILD_NATIVE_BINARY" = true ]; then
|
||||
msg_info "▶ Building native binary"
|
||||
CMAKE_BUILD_TYPE=Debug make build_native
|
||||
msg_info "▶ Checking if GDB is available on remote host"
|
||||
if ! sshdev "command -v gdbserver > /dev/null 2>&1"; then
|
||||
msg_warn "Error: gdbserver is not installed on the remote host"
|
||||
tar -czf - -C /opt/jetkvm-native-buildkit/gdb/ . | sshdev "tar -xzf - -C /usr/bin"
|
||||
msg_info "✓ gdbserver installed on remote host"
|
||||
fi
|
||||
msg_info "▶ Stopping any existing instances of jetkvm_native_debug on remote host"
|
||||
sshdev "killall -9 jetkvm_app jetkvm_app_debug jetkvm_native_debug gdbserver || true >> /dev/null 2>&1"
|
||||
sshdev "cat > ${REMOTE_PATH}/jetkvm_native_debug" < internal/native/cgo/build/jknative-bin
|
||||
sshdev -t ash << EOF
|
||||
set -e
|
||||
|
||||
# Set the library path to include the directory where librockit.so is located
|
||||
export LD_LIBRARY_PATH=/oem/usr/lib:\$LD_LIBRARY_PATH
|
||||
|
||||
cd ${REMOTE_PATH}
|
||||
killall -9 jetkvm_app jetkvm_app_debug jetkvm_native_debug || true
|
||||
sleep 5
|
||||
echo 'V' > /dev/watchdog
|
||||
chmod +x jetkvm_native_debug
|
||||
gdbserver localhost:${GDB_DEBUG_PORT} ./jetkvm_native_debug
|
||||
EOF
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Build the development version on the host
|
||||
# When using `make build_release`, the frontend will be built regardless of the `SKIP_UI_BUILD` flag
|
||||
# check if static/index.html exists
|
||||
|
|
@ -174,7 +160,7 @@ if [[ "$SKIP_UI_BUILD" = true && ! -f "static/index.html" ]]; then
|
|||
SKIP_UI_BUILD=false
|
||||
fi
|
||||
|
||||
if [[ "$SKIP_UI_BUILD" = false && "$JETKVM_INSIDE_DOCKER" != 1 ]]; then
|
||||
if [[ "$SKIP_UI_BUILD" = false && "$JETKVM_INSIDE_DOCKER" != 1 ]]; then
|
||||
msg_info "▶ Building frontend"
|
||||
make frontend SKIP_UI_BUILD=0
|
||||
SKIP_UI_BUILD_RELEASE=1
|
||||
|
|
@ -187,13 +173,13 @@ fi
|
|||
|
||||
if [ "$RUN_GO_TESTS" = true ]; then
|
||||
msg_info "▶ Building go tests"
|
||||
make build_dev_test
|
||||
make build_dev_test
|
||||
|
||||
msg_info "▶ Copying device-tests.tar.gz to remote host"
|
||||
sshdev "cat > /tmp/device-tests.tar.gz" < device-tests.tar.gz
|
||||
ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" "cat > /tmp/device-tests.tar.gz" < device-tests.tar.gz
|
||||
|
||||
msg_info "▶ Running go tests"
|
||||
sshdev ash << 'EOF'
|
||||
ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" ash << 'EOF'
|
||||
set -e
|
||||
TMP_DIR=$(mktemp -d)
|
||||
cd ${TMP_DIR}
|
||||
|
|
@ -230,33 +216,39 @@ fi
|
|||
if [ "$INSTALL_APP" = true ]
|
||||
then
|
||||
msg_info "▶ Building release binary"
|
||||
do_make build_release SKIP_NATIVE_IF_EXISTS=${SKIP_NATIVE_BUILD} SKIP_UI_BUILD=${SKIP_UI_BUILD_RELEASE}
|
||||
|
||||
do_make build_release \
|
||||
SKIP_NATIVE_IF_EXISTS=${SKIP_NATIVE_BUILD} \
|
||||
SKIP_UI_BUILD=${SKIP_UI_BUILD_RELEASE} \
|
||||
ENABLE_SYNC_TRACE=${ENABLE_SYNC_TRACE}
|
||||
|
||||
# Copy the binary to the remote host as if we were the OTA updater.
|
||||
sshdev "cat > /userdata/jetkvm/jetkvm_app.update" < bin/jetkvm_app
|
||||
ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" "cat > /userdata/jetkvm/jetkvm_app.update" < bin/jetkvm_app
|
||||
|
||||
# Reboot the device, the new app will be deployed by the startup process.
|
||||
sshdev "reboot"
|
||||
ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" "reboot"
|
||||
else
|
||||
msg_info "▶ Building development binary"
|
||||
do_make build_dev SKIP_NATIVE_IF_EXISTS=${SKIP_NATIVE_BUILD} SKIP_UI_BUILD=${SKIP_UI_BUILD_RELEASE}
|
||||
|
||||
do_make build_dev \
|
||||
SKIP_NATIVE_IF_EXISTS=${SKIP_NATIVE_BUILD} \
|
||||
SKIP_UI_BUILD=${SKIP_UI_BUILD_RELEASE} \
|
||||
ENABLE_SYNC_TRACE=${ENABLE_SYNC_TRACE}
|
||||
|
||||
# Kill any existing instances of the application
|
||||
sshdev "killall jetkvm_app_debug || true"
|
||||
ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" "killall jetkvm_app_debug || true"
|
||||
|
||||
# Copy the binary to the remote host
|
||||
sshdev "cat > ${REMOTE_PATH}/jetkvm_app_debug" < bin/jetkvm_app
|
||||
ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" "cat > ${REMOTE_PATH}/jetkvm_app_debug" < bin/jetkvm_app
|
||||
|
||||
if [ "$RESET_USB_HID_DEVICE" = true ]; then
|
||||
msg_info "▶ Resetting USB HID device"
|
||||
msg_warn "The option has been deprecated and will be removed in a future version, as JetKVM will now reset USB gadget configuration when needed"
|
||||
# Remove the old USB gadget configuration
|
||||
sshdev "rm -rf /sys/kernel/config/usb_gadget/jetkvm/configs/c.1/hid.usb*"
|
||||
sshdev "ls /sys/class/udc > /sys/kernel/config/usb_gadget/jetkvm/UDC"
|
||||
ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" "rm -rf /sys/kernel/config/usb_gadget/jetkvm/configs/c.1/hid.usb*"
|
||||
ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" "ls /sys/class/udc > /sys/kernel/config/usb_gadget/jetkvm/UDC"
|
||||
fi
|
||||
|
||||
|
||||
# Deploy and run the application on the remote host
|
||||
sshdev ash << EOF
|
||||
ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" ash << EOF
|
||||
set -e
|
||||
|
||||
# Set the library path to include the directory where librockit.so is located
|
||||
|
|
@ -266,6 +258,17 @@ export LD_LIBRARY_PATH=/oem/usr/lib:\$LD_LIBRARY_PATH
|
|||
killall jetkvm_app || true
|
||||
killall jetkvm_app_debug || true
|
||||
|
||||
# Wait until both binaries are killed, max 10 seconds
|
||||
i=1
|
||||
while [ \$i -le 10 ]; do
|
||||
echo "Waiting for jetkvm_app and jetkvm_app_debug to be killed, \$i/10 ..."
|
||||
if ! pgrep -f "jetkvm_app" > /dev/null && ! pgrep -f "jetkvm_app_debug" > /dev/null; then
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
i=\$((i + 1))
|
||||
done
|
||||
|
||||
# Navigate to the directory where the binary will be stored
|
||||
cd "${REMOTE_PATH}"
|
||||
|
||||
|
|
|
|||
|
|
@ -572,14 +572,21 @@ export interface OtaState {
|
|||
export interface UpdateState {
|
||||
isUpdatePending: boolean;
|
||||
setIsUpdatePending: (isPending: boolean) => void;
|
||||
|
||||
updateDialogHasBeenMinimized: boolean;
|
||||
setUpdateDialogHasBeenMinimized: (hasBeenMinimized: boolean) => void;
|
||||
|
||||
otaState: OtaState;
|
||||
setOtaState: (state: OtaState) => void;
|
||||
setUpdateDialogHasBeenMinimized: (hasBeenMinimized: boolean) => void;
|
||||
|
||||
modalView: UpdateModalViews;
|
||||
setModalView: (view: UpdateModalViews) => void;
|
||||
setUpdateErrorMessage: (errorMessage: string) => void;
|
||||
|
||||
updateErrorMessage: string | null;
|
||||
setUpdateErrorMessage: (errorMessage: string) => void;
|
||||
|
||||
shouldReload: boolean;
|
||||
setShouldReload: (reloadRequired: boolean) => void;
|
||||
}
|
||||
|
||||
export const useUpdateStore = create<UpdateState>(set => ({
|
||||
|
|
@ -610,11 +617,16 @@ export const useUpdateStore = create<UpdateState>(set => ({
|
|||
updateDialogHasBeenMinimized: false,
|
||||
setUpdateDialogHasBeenMinimized: (hasBeenMinimized: boolean) =>
|
||||
set({ updateDialogHasBeenMinimized: hasBeenMinimized }),
|
||||
|
||||
modalView: "loading",
|
||||
setModalView: (view: UpdateModalViews) => set({ modalView: view }),
|
||||
|
||||
updateErrorMessage: null,
|
||||
setUpdateErrorMessage: (errorMessage: string) =>
|
||||
set({ updateErrorMessage: errorMessage }),
|
||||
|
||||
shouldReload: false,
|
||||
setShouldReload: (reloadRequired: boolean) => set({ shouldReload: reloadRequired }),
|
||||
}));
|
||||
|
||||
export type UsbConfigModalViews = "updateUsbConfig" | "updateUsbConfigSuccess";
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { useCallback } from "react";
|
||||
|
||||
import { useDeviceStore } from "@/hooks/stores";
|
||||
import { type JsonRpcResponse, RpcMethodNotFound, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import { JsonRpcError, RpcMethodNotFound } from "@/hooks/useJsonRpc";
|
||||
import { getUpdateStatus, getLocalVersion as getLocalVersionRpc } from "@/utils/jsonrpc";
|
||||
import notifications from "@/notifications";
|
||||
|
||||
export interface VersionInfo {
|
||||
|
|
@ -17,19 +18,6 @@ export interface SystemVersionInfo {
|
|||
error?: string;
|
||||
}
|
||||
|
||||
export interface VersionInfo {
|
||||
appVersion: string;
|
||||
systemVersion: string;
|
||||
}
|
||||
|
||||
export interface SystemVersionInfo {
|
||||
local: VersionInfo;
|
||||
remote?: VersionInfo;
|
||||
systemUpdateAvailable: boolean;
|
||||
appUpdateAvailable: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export function useVersion() {
|
||||
const {
|
||||
appVersion,
|
||||
|
|
@ -37,51 +25,40 @@ export function useVersion() {
|
|||
setAppVersion,
|
||||
setSystemVersion,
|
||||
} = useDeviceStore();
|
||||
const { send } = useJsonRpc();
|
||||
const getVersionInfo = useCallback(() => {
|
||||
return new Promise<SystemVersionInfo>((resolve, reject) => {
|
||||
send("getUpdateStatus", {}, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(`Failed to check for updates: ${resp.error}`);
|
||||
reject(new Error("Failed to check for updates"));
|
||||
} else {
|
||||
const result = resp.result as SystemVersionInfo;
|
||||
setAppVersion(result.local.appVersion);
|
||||
setSystemVersion(result.local.systemVersion);
|
||||
|
||||
if (result.error) {
|
||||
notifications.error(`Failed to check for updates: ${result.error}`);
|
||||
reject(new Error("Failed to check for updates"));
|
||||
} else {
|
||||
resolve(result);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}, [send, setAppVersion, setSystemVersion]);
|
||||
const getVersionInfo = useCallback(async () => {
|
||||
try {
|
||||
const result = await getUpdateStatus();
|
||||
setAppVersion(result.local.appVersion);
|
||||
setSystemVersion(result.local.systemVersion);
|
||||
return result;
|
||||
} catch (error) {
|
||||
const jsonRpcError = error as JsonRpcError;
|
||||
notifications.error(`Failed to check for updates: ${jsonRpcError.message}`);
|
||||
throw jsonRpcError;
|
||||
}
|
||||
}, [setAppVersion, setSystemVersion]);
|
||||
|
||||
const getLocalVersion = useCallback(() => {
|
||||
return new Promise<VersionInfo>((resolve, reject) => {
|
||||
send("getLocalVersion", {}, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
console.log(resp.error)
|
||||
if (resp.error.code === RpcMethodNotFound) {
|
||||
console.warn("Failed to get device version, using legacy version");
|
||||
return getVersionInfo().then(result => resolve(result.local)).catch(reject);
|
||||
}
|
||||
console.error("Failed to get device version N", resp.error);
|
||||
notifications.error(`Failed to get device version: ${resp.error}`);
|
||||
reject(new Error("Failed to get device version"));
|
||||
} else {
|
||||
const result = resp.result as VersionInfo;
|
||||
const getLocalVersion = useCallback(async () => {
|
||||
try {
|
||||
const result = await getLocalVersionRpc();
|
||||
setAppVersion(result.appVersion);
|
||||
setSystemVersion(result.systemVersion);
|
||||
return result;
|
||||
} catch (error: unknown) {
|
||||
const jsonRpcError = error as JsonRpcError;
|
||||
|
||||
setAppVersion(result.appVersion);
|
||||
setSystemVersion(result.systemVersion);
|
||||
resolve(result);
|
||||
}
|
||||
});
|
||||
});
|
||||
}, [send, setAppVersion, setSystemVersion, getVersionInfo]);
|
||||
if (jsonRpcError.code === RpcMethodNotFound) {
|
||||
console.error("Failed to get local version, using legacy remote version");
|
||||
const result = await getVersionInfo();
|
||||
return result.local;
|
||||
}
|
||||
|
||||
console.error("Failed to get device version", jsonRpcError);
|
||||
notifications.error(`Failed to get device version: ${jsonRpcError.message}`);
|
||||
throw jsonRpcError;
|
||||
}
|
||||
}, [setAppVersion, setSystemVersion, getVersionInfo]);
|
||||
|
||||
return {
|
||||
getVersionInfo,
|
||||
|
|
@ -89,4 +66,4 @@ export function useVersion() {
|
|||
appVersion,
|
||||
systemVersion,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,23 @@
|
|||
import { useLoaderData, useNavigate } from "react-router";
|
||||
import type { LoaderFunction } from "react-router";
|
||||
import { ShieldCheckIcon } from "@heroicons/react/24/outline";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useLoaderData, useNavigate, type LoaderFunction } from "react-router";
|
||||
import { ShieldCheckIcon } from "@heroicons/react/24/outline";
|
||||
|
||||
import api from "@/api";
|
||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||
import { GridCard } from "@/components/Card";
|
||||
import { Button, LinkButton } from "@/components/Button";
|
||||
import { InputFieldWithLabel } from "@/components/InputField";
|
||||
import { SelectMenuBasic } from "@/components/SelectMenuBasic";
|
||||
import { useDeviceUiNavigation } from "@hooks/useAppNavigation";
|
||||
import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
|
||||
import { GridCard } from "@components/Card";
|
||||
import { Button, LinkButton } from "@components/Button";
|
||||
import { InputFieldWithLabel } from "@components/InputField";
|
||||
import { SelectMenuBasic } from "@components/SelectMenuBasic";
|
||||
import { SettingsItem } from "@components/SettingsItem";
|
||||
import { SettingsSectionHeader } from "@/components/SettingsSectionHeader";
|
||||
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
|
||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||
import { SettingsSectionHeader } from "@components/SettingsSectionHeader";
|
||||
import { NestedSettingsGroup } from "@components/NestedSettingsGroup";
|
||||
import { TextAreaWithLabel } from "@components/TextArea";
|
||||
import api from "@/api";
|
||||
import notifications from "@/notifications";
|
||||
import { DEVICE_API } from "@/ui.config";
|
||||
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import { isOnDevice } from "@/main";
|
||||
import { TextAreaWithLabel } from "@components/TextArea";
|
||||
import { m } from "@localizations/messages.js";
|
||||
|
||||
import { LocalDevice } from "./devices.$id";
|
||||
import { CloudState } from "./adopt";
|
||||
|
|
@ -92,13 +93,13 @@ export default function SettingsAccessIndexRoute() {
|
|||
send("deregisterDevice", {}, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to de-register device: ${resp.error.data || "Unknown error"}`,
|
||||
m.access_failed_deregister({ error: resp.error.data || m.unknown_error() }),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
getCloudState();
|
||||
// In cloud mode, we need to navigate to the device overview page, as we don't a connection anymore
|
||||
// In cloud mode, we need to navigate to the device overview page, as we don't have a connection anymore
|
||||
if (!isOnDevice) navigate("/");
|
||||
return;
|
||||
});
|
||||
|
|
@ -107,14 +108,14 @@ export default function SettingsAccessIndexRoute() {
|
|||
const onCloudAdoptClick = useCallback(
|
||||
(cloudApiUrl: string, cloudAppUrl: string) => {
|
||||
if (!deviceId) {
|
||||
notifications.error("No device ID available");
|
||||
notifications.error(m.access_no_device_id());
|
||||
return;
|
||||
}
|
||||
|
||||
send("setCloudUrl", { apiUrl: cloudApiUrl, appUrl: cloudAppUrl }, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to update cloud URL: ${resp.error.data || "Unknown error"}`,
|
||||
m.access_failed_update_cloud_url({ error: resp.error.data || m.unknown_error() }),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
@ -160,12 +161,12 @@ export default function SettingsAccessIndexRoute() {
|
|||
send("setTLSState", { state }, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to update TLS settings: ${resp.error.data || "Unknown error"}`,
|
||||
m.access_failed_update_tls({ error: resp.error.data || m.unknown_error() }),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
notifications.success("TLS settings updated successfully");
|
||||
notifications.success(m.access_tls_updated());
|
||||
});
|
||||
}, [send]);
|
||||
|
||||
|
|
@ -206,22 +207,22 @@ export default function SettingsAccessIndexRoute() {
|
|||
return (
|
||||
<div className="space-y-4">
|
||||
<SettingsPageHeader
|
||||
title="Access"
|
||||
description="Manage the Access Control of the device"
|
||||
title={m.access_title()}
|
||||
description={m.access_description()}
|
||||
/>
|
||||
|
||||
{loaderData?.authMode && (
|
||||
<>
|
||||
<div className="space-y-4">
|
||||
<SettingsSectionHeader
|
||||
title="Local"
|
||||
description="Manage the mode of local access to the device"
|
||||
title={m.access_local_title()}
|
||||
description={m.access_local_description()}
|
||||
/>
|
||||
<>
|
||||
<SettingsItem
|
||||
title="HTTPS Mode"
|
||||
title={m.access_https_mode_title()}
|
||||
badge="Experimental"
|
||||
description="Configure secure HTTPS access to your device"
|
||||
description={m.access_https_description()}
|
||||
>
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
|
|
@ -229,52 +230,43 @@ export default function SettingsAccessIndexRoute() {
|
|||
onChange={e => handleTlsModeChange(e.target.value)}
|
||||
disabled={tlsMode === "unknown"}
|
||||
options={[
|
||||
{ value: "disabled", label: "Disabled" },
|
||||
{ value: "self-signed", label: "Self-signed" },
|
||||
{ value: "custom", label: "Custom" },
|
||||
{ value: "disabled", label: m.access_tls_disabled() },
|
||||
{ value: "self-signed", label: m.access_tls_self_signed() },
|
||||
{ value: "custom", label: m.access_tls_custom() },
|
||||
]}
|
||||
/>
|
||||
</SettingsItem>
|
||||
|
||||
{tlsMode === "custom" && (
|
||||
<div className="mt-4 space-y-4">
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title="TLS Certificate"
|
||||
description="Paste your TLS certificate below. For certificate chains, include the entire chain (leaf, intermediate, and root certificates)."
|
||||
/>
|
||||
<div className="space-y-4">
|
||||
<TextAreaWithLabel
|
||||
label="Certificate"
|
||||
rows={3}
|
||||
placeholder={
|
||||
"-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----"
|
||||
}
|
||||
value={tlsCert}
|
||||
onChange={e => handleTlsCertChange(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-4">
|
||||
<TextAreaWithLabel
|
||||
label="Private Key"
|
||||
description="For security reasons, it will not be displayed after saving."
|
||||
rows={3}
|
||||
placeholder={
|
||||
"-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----"
|
||||
}
|
||||
value={tlsKey}
|
||||
onChange={e => handleTlsKeyChange(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<NestedSettingsGroup className="mt-4">
|
||||
<SettingsItem
|
||||
title={m.access_tls_certificate_title()}
|
||||
description={m.access_tls_certificate_description()}
|
||||
/>
|
||||
<TextAreaWithLabel
|
||||
label={m.access_certificate_label()}
|
||||
rows={3}
|
||||
placeholder={
|
||||
"-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----"
|
||||
}
|
||||
value={tlsCert}
|
||||
onChange={e => handleTlsCertChange(e.target.value)}
|
||||
/>
|
||||
<TextAreaWithLabel
|
||||
label={m.access_private_key_label()}
|
||||
description={m.access_private_key_description()}
|
||||
rows={3}
|
||||
placeholder={
|
||||
"-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----"
|
||||
}
|
||||
value={tlsKey}
|
||||
onChange={e => handleTlsKeyChange(e.target.value)}
|
||||
/>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Button
|
||||
size="SM"
|
||||
theme="primary"
|
||||
text="Update TLS Settings"
|
||||
text={m.access_update_tls_settings()}
|
||||
onClick={handleCustomTlsUpdate}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -282,14 +274,14 @@ export default function SettingsAccessIndexRoute() {
|
|||
)}
|
||||
|
||||
<SettingsItem
|
||||
title="Authentication Mode"
|
||||
description={`Current mode: ${loaderData.authMode === "password" ? "Password protected" : "No password"}`}
|
||||
title={m.access_authentication_mode_title()}
|
||||
description={loaderData.authMode === "password" ? m.access_auth_mode_password() : m.access_auth_mode_no_password()}
|
||||
>
|
||||
{loaderData.authMode === "password" ? (
|
||||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
text="Disable Protection"
|
||||
text={m.access_disable_protection()}
|
||||
onClick={() => {
|
||||
navigateTo("./local-auth", { state: { init: "deletePassword" } });
|
||||
}}
|
||||
|
|
@ -298,7 +290,7 @@ export default function SettingsAccessIndexRoute() {
|
|||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
text="Enable Password"
|
||||
text={m.access_enable_password()}
|
||||
onClick={() => {
|
||||
navigateTo("./local-auth", { state: { init: "createPassword" } });
|
||||
}}
|
||||
|
|
@ -309,13 +301,13 @@ export default function SettingsAccessIndexRoute() {
|
|||
|
||||
{loaderData.authMode === "password" && (
|
||||
<SettingsItem
|
||||
title="Change Password"
|
||||
description="Update your device access password"
|
||||
title={m.access_change_password_title()}
|
||||
description={m.access_change_password_description()}
|
||||
>
|
||||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
text="Change Password"
|
||||
text={m.access_change_password_button()}
|
||||
onClick={() => {
|
||||
navigateTo("./local-auth", { state: { init: "updatePassword" } });
|
||||
}}
|
||||
|
|
@ -330,23 +322,23 @@ export default function SettingsAccessIndexRoute() {
|
|||
<div className="space-y-4">
|
||||
<SettingsSectionHeader
|
||||
title="Remote"
|
||||
description="Manage the mode of Remote access to the device"
|
||||
description={m.access_remote_description()}
|
||||
/>
|
||||
|
||||
<div className="space-y-4">
|
||||
{!isAdopted && (
|
||||
<>
|
||||
<SettingsItem
|
||||
title="Cloud Provider"
|
||||
description="Select the cloud provider for your device"
|
||||
title={m.access_cloud_provider_title()}
|
||||
description={m.access_cloud_provider_description()}
|
||||
>
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
value={selectedProvider}
|
||||
onChange={e => handleProviderChange(e.target.value)}
|
||||
options={[
|
||||
{ value: "jetkvm", label: "JetKVM Cloud" },
|
||||
{ value: "custom", label: "Custom" },
|
||||
{ value: "jetkvm", label: m.access_provider_jetkvm() },
|
||||
{ value: "custom", label: m.access_provider_custom() },
|
||||
]}
|
||||
/>
|
||||
</SettingsItem>
|
||||
|
|
@ -356,7 +348,7 @@ export default function SettingsAccessIndexRoute() {
|
|||
<div className="flex items-end gap-x-2">
|
||||
<InputFieldWithLabel
|
||||
size="SM"
|
||||
label="Cloud API URL"
|
||||
label={m.access_cloud_api_url_label()}
|
||||
value={cloudApiUrl}
|
||||
onChange={e => setCloudApiUrl(e.target.value)}
|
||||
placeholder="https://api.example.com"
|
||||
|
|
@ -365,7 +357,7 @@ export default function SettingsAccessIndexRoute() {
|
|||
<div className="flex items-end gap-x-2">
|
||||
<InputFieldWithLabel
|
||||
size="SM"
|
||||
label="Cloud App URL"
|
||||
label={m.access_cloud_app_url_label()}
|
||||
value={cloudAppUrl}
|
||||
onChange={e => setCloudAppUrl(e.target.value)}
|
||||
placeholder="https://app.example.com"
|
||||
|
|
@ -384,26 +376,26 @@ export default function SettingsAccessIndexRoute() {
|
|||
<div className="space-y-3">
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-base font-bold text-slate-900 dark:text-white">
|
||||
Cloud Security
|
||||
{m.access_cloud_security_title()}
|
||||
</h3>
|
||||
<div>
|
||||
<ul className="list-disc space-y-1 pl-5 text-xs text-slate-700 dark:text-slate-300">
|
||||
<li>End-to-end encryption using WebRTC (DTLS and SRTP)</li>
|
||||
<li>Zero Trust security model</li>
|
||||
<li>OIDC (OpenID Connect) authentication</li>
|
||||
<li>All streams encrypted in transit</li>
|
||||
<li>{m.access_security_encryption()}</li>
|
||||
<li>{m.access_security_zero_trust()}</li>
|
||||
<li>{m.access_security_oidc()}</li>
|
||||
<li>{m.access_security_streams()}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-slate-700 dark:text-slate-300">
|
||||
All cloud components are open-source and available on{" "}
|
||||
{m.access_security_open_source()}{" "}
|
||||
<a
|
||||
href="https://github.com/jetkvm"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-medium text-blue-600 hover:text-blue-800 dark:text-blue-500 dark:hover:text-blue-400"
|
||||
>
|
||||
GitHub
|
||||
{m.access_github_link()}
|
||||
</a>
|
||||
.
|
||||
</div>
|
||||
|
|
@ -415,7 +407,7 @@ export default function SettingsAccessIndexRoute() {
|
|||
to="https://jetkvm.com/docs/networking/remote-access"
|
||||
size="SM"
|
||||
theme="light"
|
||||
text="Learn about our cloud security"
|
||||
text={m.access_learn_security()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -429,32 +421,32 @@ export default function SettingsAccessIndexRoute() {
|
|||
onClick={() => onCloudAdoptClick(cloudApiUrl, cloudAppUrl)}
|
||||
size="SM"
|
||||
theme="primary"
|
||||
text="Adopt KVM to Cloud"
|
||||
text={m.access_adopt_kvm()}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-slate-600 dark:text-slate-300">
|
||||
Your device is adopted to the Cloud
|
||||
{m.access_adopted_message()}
|
||||
</p>
|
||||
<div>
|
||||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
text="De-register from Cloud"
|
||||
text={m.access_deregister()}
|
||||
className="text-red-600"
|
||||
onClick={() => {
|
||||
if (deviceId) {
|
||||
if (
|
||||
window.confirm(
|
||||
"Are you sure you want to de-register this device?",
|
||||
m.access_confirm_deregister(),
|
||||
)
|
||||
) {
|
||||
deregisterDevice();
|
||||
}
|
||||
} else {
|
||||
notifications.error("No device ID available");
|
||||
notifications.error(m.access_no_device_id());
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export default function SettingsGeneralUpdateRoute() {
|
|||
const [searchParams] = useSearchParams();
|
||||
const { updateSuccess } = location.state || {};
|
||||
|
||||
const { setModalView, otaState } = useUpdateStore();
|
||||
const { setModalView, otaState, shouldReload, setShouldReload } = useUpdateStore();
|
||||
const { send } = useJsonRpc();
|
||||
|
||||
const customAppVersion = useMemo(() => searchParams.get("custom_app_version") || undefined, [searchParams]);
|
||||
|
|
@ -37,6 +37,7 @@ export default function SettingsGeneralUpdateRoute() {
|
|||
}, [navigate, setShouldReload, shouldReload]);
|
||||
|
||||
const onConfirmUpdate = useCallback(() => {
|
||||
setShouldReload(true);
|
||||
send("tryUpdate", {});
|
||||
setModalView("updating");
|
||||
}, [send, setModalView, setShouldReload]);
|
||||
|
|
@ -66,7 +67,7 @@ export default function SettingsGeneralUpdateRoute() {
|
|||
} else {
|
||||
setModalView("loading");
|
||||
}
|
||||
}, [otaState.updating, otaState.error, setModalView, updateSuccess]);
|
||||
}, [otaState.error, otaState.updating, setModalView, updateSuccess]);
|
||||
|
||||
return <Dialog
|
||||
onClose={onClose}
|
||||
|
|
@ -77,8 +78,6 @@ export default function SettingsGeneralUpdateRoute() {
|
|||
/>;
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function Dialog({
|
||||
onClose,
|
||||
onConfirmUpdate,
|
||||
|
|
@ -120,11 +119,6 @@ export function Dialog({
|
|||
[setModalView, forceCustomUpdate],
|
||||
);
|
||||
|
||||
// Reset modal view when dialog is opened
|
||||
useEffect(() => {
|
||||
setVersionInfo(null);
|
||||
}, [setModalView]);
|
||||
|
||||
return (
|
||||
<div className="pointer-events-auto relative mx-auto text-left">
|
||||
<div>
|
||||
|
|
@ -189,6 +183,7 @@ function LoadingState({
|
|||
const abortControllerRef = useRef<AbortController | null>(null);
|
||||
|
||||
const { getVersionInfo } = useVersion();
|
||||
const { setModalView } = useUpdateStore();
|
||||
|
||||
const progressBarRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
|
|
@ -204,28 +199,29 @@ function LoadingState({
|
|||
}, [customAppVersion, customSystemVersion, getVersionInfo]);
|
||||
|
||||
useEffect(() => {
|
||||
setProgressWidth("0%");
|
||||
|
||||
abortControllerRef.current = new AbortController();
|
||||
const signal = abortControllerRef.current.signal;
|
||||
|
||||
const animationTimer = setTimeout(() => {
|
||||
// we start the progress bar animation after a tiny delay to avoid react warnings
|
||||
setProgressWidth("100%");
|
||||
}, 0);
|
||||
|
||||
checkUpdate()
|
||||
.then(async versionInfo => {
|
||||
// Add a small delay to ensure it's not just flickering
|
||||
return new Promise(resolve => setTimeout(() => resolve(versionInfo), 600));
|
||||
await sleep(600);
|
||||
return versionInfo
|
||||
})
|
||||
.then(versionInfo => {
|
||||
if (!signal.aborted) {
|
||||
onFinished(versionInfo as SystemVersionInfo);
|
||||
onFinished(versionInfo);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
if (!signal.aborted) {
|
||||
console.error("LoadingState: Error fetching version info", error);
|
||||
setModalView("error");
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -268,72 +264,94 @@ function UpdatingDeviceState({
|
|||
otaState: UpdateState["otaState"];
|
||||
onMinimizeUpgradeDialog: () => void;
|
||||
}) {
|
||||
const formatProgress = (progress: number) => `${Math.round(progress)}%`;
|
||||
interface ProgressSummary {
|
||||
system: UpdatePart;
|
||||
app: UpdatePart;
|
||||
areAllUpdatesComplete: boolean;
|
||||
};
|
||||
|
||||
const calculateOverallProgress = (type: "system" | "app") => {
|
||||
const downloadProgress = Math.round((otaState[`${type}DownloadProgress`] || 0) * 100);
|
||||
const updateProgress = Math.round((otaState[`${type}UpdateProgress`] || 0) * 100);
|
||||
const verificationProgress = Math.round(
|
||||
(otaState[`${type}VerificationProgress`] || 0) * 100,
|
||||
);
|
||||
|
||||
if (!downloadProgress && !updateProgress && !verificationProgress) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
console.log(
|
||||
`For ${type}:\n` +
|
||||
` Download Progress: ${downloadProgress}% (${otaState[`${type}DownloadProgress`]})\n` +
|
||||
` Update Progress: ${updateProgress}% (${otaState[`${type}UpdateProgress`]})\n` +
|
||||
` Verification Progress: ${verificationProgress}% (${otaState[`${type}VerificationProgress`]})`,
|
||||
);
|
||||
|
||||
if (type === "app") {
|
||||
// App: 65% download, 34% verification, 1% update(There is no "real" update for the app)
|
||||
return Math.min(
|
||||
downloadProgress * 0.55 + verificationProgress * 0.54 + updateProgress * 0.01,
|
||||
100,
|
||||
const progress = useMemo<ProgressSummary>(() => {
|
||||
const calculateOverallProgress = (type: "system" | "app") => {
|
||||
const downloadProgress = Math.round((otaState[`${type}DownloadProgress`] || 0) * 100);
|
||||
const updateProgress = Math.round((otaState[`${type}UpdateProgress`] || 0) * 100);
|
||||
const verificationProgress = Math.round(
|
||||
(otaState[`${type}VerificationProgress`] || 0) * 100,
|
||||
);
|
||||
|
||||
if (!downloadProgress && !updateProgress && !verificationProgress) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (type === "app") {
|
||||
// App: 55% download, 54% verification, 1% update(There is no "real" update for the app)
|
||||
return Math.round(Math.min(
|
||||
downloadProgress * 0.55 + verificationProgress * 0.54 + updateProgress * 0.01,
|
||||
100,
|
||||
));
|
||||
} else {
|
||||
// System: 10% download, 10% verification, 80% update
|
||||
return Math.round(Math.min(
|
||||
downloadProgress * 0.1 + verificationProgress * 0.1 + updateProgress * 0.8,
|
||||
100,
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
const getUpdateStatus = (type: "system" | "app") => {
|
||||
const downloadFinishedAt = otaState[`${type}DownloadFinishedAt`];
|
||||
const verifiedAt = otaState[`${type}VerifiedAt`];
|
||||
const updatedAt = otaState[`${type}UpdatedAt`];
|
||||
|
||||
|
||||
if (!otaState.metadataFetchedAt) {
|
||||
return "Fetching update information...";
|
||||
} else if (!downloadFinishedAt) {
|
||||
return `Downloading ${type} update...`;
|
||||
} else if (!verifiedAt) {
|
||||
return `Verifying ${type} update...`;
|
||||
} else if (!updatedAt) {
|
||||
return `Installing ${type} update...`;
|
||||
} else {
|
||||
return `Awaiting reboot`;
|
||||
}
|
||||
};
|
||||
|
||||
const isUpdateComplete = (type: "system" | "app") => {
|
||||
return !!otaState[`${type}UpdatedAt`];
|
||||
};
|
||||
|
||||
const systemUpdatePending = otaState.systemUpdatePending
|
||||
const systemUpdateComplete = isUpdateComplete("system");
|
||||
|
||||
const appUpdatePending = otaState.appUpdatePending
|
||||
const appUpdateComplete = isUpdateComplete("app");
|
||||
|
||||
let areAllUpdatesComplete: boolean;
|
||||
if (!systemUpdatePending && !appUpdatePending) {
|
||||
areAllUpdatesComplete = false;
|
||||
} else if (systemUpdatePending && appUpdatePending) {
|
||||
areAllUpdatesComplete = systemUpdateComplete && appUpdateComplete;
|
||||
} else {
|
||||
// System: 10% download, 90% update
|
||||
return Math.min(
|
||||
downloadProgress * 0.1 + verificationProgress * 0.1 + updateProgress * 0.8,
|
||||
100,
|
||||
);
|
||||
areAllUpdatesComplete = systemUpdatePending ? systemUpdateComplete : appUpdateComplete;
|
||||
}
|
||||
};
|
||||
|
||||
const getUpdateStatus = (type: "system" | "app") => {
|
||||
const downloadFinishedAt = otaState[`${type}DownloadFinishedAt`];
|
||||
const verfiedAt = otaState[`${type}VerifiedAt`];
|
||||
const updatedAt = otaState[`${type}UpdatedAt`];
|
||||
return {
|
||||
system: {
|
||||
pending: systemUpdatePending,
|
||||
status: getUpdateStatus("system"),
|
||||
progress: calculateOverallProgress("system"),
|
||||
complete: systemUpdateComplete,
|
||||
},
|
||||
app: {
|
||||
pending: appUpdatePending,
|
||||
status: getUpdateStatus("app"),
|
||||
progress: calculateOverallProgress("app"),
|
||||
complete: appUpdateComplete,
|
||||
},
|
||||
areAllUpdatesComplete,
|
||||
};
|
||||
}, [otaState]);
|
||||
|
||||
if (!otaState.metadataFetchedAt) {
|
||||
return "Fetching update information...";
|
||||
} else if (!downloadFinishedAt) {
|
||||
return `Downloading ${type} update...`;
|
||||
} else if (!verfiedAt) {
|
||||
return `Verifying ${type} update...`;
|
||||
} else if (!updatedAt) {
|
||||
return `Installing ${type} update...`;
|
||||
} else {
|
||||
return `Awaiting reboot`;
|
||||
}
|
||||
};
|
||||
|
||||
const isUpdateComplete = (type: "system" | "app") => {
|
||||
return !!otaState[`${type}UpdatedAt`];
|
||||
};
|
||||
|
||||
const areAllUpdatesComplete = () => {
|
||||
if (otaState.systemUpdatePending && otaState.appUpdatePending) {
|
||||
return isUpdateComplete("system") && isUpdateComplete("app");
|
||||
}
|
||||
return (
|
||||
(otaState.systemUpdatePending && isUpdateComplete("system")) ||
|
||||
(otaState.appUpdatePending && isUpdateComplete("app"))
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-start justify-start space-y-4 text-left">
|
||||
|
|
@ -347,7 +365,7 @@ function UpdatingDeviceState({
|
|||
</p>
|
||||
</div>
|
||||
<Card className="space-y-4 p-4">
|
||||
{areAllUpdatesComplete() ? (
|
||||
{progress.areAllUpdatesComplete ? (
|
||||
<div className="my-2 flex flex-col items-center space-y-2 text-center">
|
||||
<LoadingSpinner className="h-6 w-6 text-blue-700 dark:text-blue-500" />
|
||||
<div className="flex justify-between text-sm text-slate-600 dark:text-slate-300">
|
||||
|
|
@ -358,72 +376,22 @@ function UpdatingDeviceState({
|
|||
</div>
|
||||
) : (
|
||||
<>
|
||||
{!(otaState.systemUpdatePending || otaState.appUpdatePending) && (
|
||||
{!(progress.system.pending || progress.app.pending) && (
|
||||
<div className="my-2 flex flex-col items-center space-y-2 text-center">
|
||||
<LoadingSpinner className="h-6 w-6 text-blue-700 dark:text-blue-500" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{otaState.systemUpdatePending && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm font-semibold text-black dark:text-white">
|
||||
Linux System Update
|
||||
</p>
|
||||
{calculateOverallProgress("system") < 100 ? (
|
||||
<LoadingSpinner className="h-4 w-4 text-blue-700 dark:text-blue-500" />
|
||||
) : (
|
||||
<CheckCircleIcon className="h-4 w-4 text-blue-700 dark:text-blue-500" />
|
||||
)}
|
||||
</div>
|
||||
<div className="h-2.5 w-full overflow-hidden rounded-full bg-slate-300 dark:bg-slate-600">
|
||||
<div
|
||||
className="h-2.5 rounded-full bg-blue-700 transition-all duration-500 ease-linear dark:bg-blue-500"
|
||||
style={{
|
||||
width: formatProgress(calculateOverallProgress("system")),
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm text-slate-600 dark:text-slate-300">
|
||||
<span>{getUpdateStatus("system")}</span>
|
||||
{calculateOverallProgress("system") < 100 ? (
|
||||
<span>{formatProgress(calculateOverallProgress("system"))}</span>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
{progress.system.pending && (
|
||||
<UpdatingStatusCard label="Linux System Update" part={progress.system} />
|
||||
)}
|
||||
{otaState.appUpdatePending && (
|
||||
<>
|
||||
{otaState.systemUpdatePending && (
|
||||
<hr className="dark:border-slate-600" />
|
||||
)}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm font-semibold text-black dark:text-white">
|
||||
App Update
|
||||
</p>
|
||||
{calculateOverallProgress("app") < 100 ? (
|
||||
<LoadingSpinner className="h-4 w-4 text-blue-700 dark:text-blue-500" />
|
||||
) : (
|
||||
<CheckCircleIcon className="h-4 w-4 text-blue-700 dark:text-blue-500" />
|
||||
)}
|
||||
</div>
|
||||
<div className="h-2.5 w-full overflow-hidden rounded-full bg-slate-300 dark:bg-slate-600">
|
||||
<div
|
||||
className="h-2.5 rounded-full bg-blue-700 transition-all duration-500 ease-linear dark:bg-blue-500"
|
||||
style={{
|
||||
width: formatProgress(calculateOverallProgress("app")),
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm text-slate-600 dark:text-slate-300">
|
||||
<span>{getUpdateStatus("app")}</span>
|
||||
{calculateOverallProgress("system") < 100 ? (
|
||||
<span>{formatProgress(calculateOverallProgress("app"))}</span>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
{progress.system.pending && progress.app.pending && (
|
||||
<hr className="dark:border-slate-600" />
|
||||
)}
|
||||
|
||||
{progress.app.pending && (
|
||||
<UpdatingStatusCard label="App Update" part={progress.app} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
|
@ -481,7 +449,7 @@ function UpdateAvailableState({
|
|||
<div className="flex flex-col items-start justify-start space-y-4 text-left">
|
||||
<div className="text-left">
|
||||
<p className="text-base font-semibold text-black dark:text-white">
|
||||
Update available
|
||||
Update Available
|
||||
</p>
|
||||
<p className="mb-2 text-sm text-slate-600 dark:text-slate-300">
|
||||
A new update is available to enhance system performance and improve
|
||||
|
|
@ -490,24 +458,24 @@ function UpdateAvailableState({
|
|||
<p className="mb-4 text-sm text-slate-600 dark:text-slate-300">
|
||||
{versionInfo?.systemUpdateAvailable ? (
|
||||
<>
|
||||
<span className="font-semibold">{m.general_update_system_type()}</span>: {versionInfo?.local?.systemVersion} <span className="text-slate-600 dark:text-slate-300">→</span> {versionInfo?.remote?.systemVersion}
|
||||
<span className="font-semibold">Linux System Update</span>: {versionInfo?.local?.systemVersion} <span className="text-slate-600 dark:text-slate-300">→</span> {versionInfo?.remote?.systemVersion}
|
||||
<br />
|
||||
</>
|
||||
) : null}
|
||||
{versionInfo?.appUpdateAvailable ? (
|
||||
<>
|
||||
<span className="font-semibold">{m.general_update_application_type()}</span>: {versionInfo?.local?.appVersion} <span className="text-slate-600 dark:text-slate-300">→</span> {versionInfo?.remote?.appVersion}
|
||||
<span className="font-semibold">App Update</span>: {versionInfo?.local?.appVersion} <span className="text-slate-600 dark:text-slate-300">→</span> {versionInfo?.remote?.appVersion}
|
||||
</>
|
||||
) : null}
|
||||
{versionInfo?.willDisableAutoUpdate ? (
|
||||
<p className="mb-4 text-sm text-red-600 dark:text-red-400">
|
||||
{m.general_update_will_disable_auto_update_description()}
|
||||
You{"'"}re about to manually change your device version. Auto-update will be disabled after the update is completed to prevent accidental updates.
|
||||
</p>
|
||||
) : null}
|
||||
</p>
|
||||
<div className="flex items-center justify-start gap-x-2">
|
||||
<Button size="SM" theme="primary" text={m.general_update_now_button()} onClick={onConfirm} />
|
||||
<Button size="SM" theme="light" text={m.general_update_later_button()} onClick={onClose} />
|
||||
<Button size="SM" theme="primary" text="Update Now" onClick={onConfirm} />
|
||||
<Button size="SM" theme="light" text="Do it later" onClick={onClose} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,228 +1,502 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useSettingsStore } from "@hooks/stores";
|
||||
import { JsonRpcError, JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
|
||||
import { useDeviceUiNavigation } from "@hooks/useAppNavigation";
|
||||
import { SystemVersionInfo } from "@hooks/useVersion";
|
||||
|
||||
import { Button } from "@components/Button";
|
||||
import Checkbox, { CheckboxWithLabel } from "@components/Checkbox";
|
||||
import { ConfirmDialog } from "@components/ConfirmDialog";
|
||||
import { GridCard } from "@components/Card";
|
||||
import { SettingsItem } from "@components/SettingsItem";
|
||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||
import { SettingsSectionHeader } from "@components/SettingsSectionHeader";
|
||||
import { BacklightSettings, useSettingsStore } from "@/hooks/stores";
|
||||
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import { NestedSettingsGroup } from "@components/NestedSettingsGroup";
|
||||
import { TextAreaWithLabel } from "@components/TextArea";
|
||||
import { InputFieldWithLabel } from "@components/InputField";
|
||||
import { SelectMenuBasic } from "@components/SelectMenuBasic";
|
||||
import { UsbDeviceSetting } from "@components/UsbDeviceSetting";
|
||||
import { Checkbox } from "@components/Checkbox";
|
||||
import { isOnDevice } from "@/main";
|
||||
import notifications from "@/notifications";
|
||||
import { sleep } from "@/utils";
|
||||
import { checkUpdateComponents, UpdateComponents } from "@/utils/jsonrpc";
|
||||
|
||||
|
||||
import notifications from "../notifications";
|
||||
import { UsbInfoSetting } from "../components/UsbInfoSetting";
|
||||
import { FeatureFlag } from "../components/FeatureFlag";
|
||||
|
||||
export default function SettingsHardwareRoute() {
|
||||
export default function SettingsAdvancedRoute() {
|
||||
const { send } = useJsonRpc();
|
||||
const { navigateTo } = useDeviceUiNavigation();
|
||||
|
||||
const [sshKey, setSSHKey] = useState<string>("");
|
||||
const { setDeveloperMode } = useSettingsStore();
|
||||
const [devChannel, setDevChannel] = useState(false);
|
||||
const [usbEmulationEnabled, setUsbEmulationEnabled] = useState(false);
|
||||
const [showLoopbackWarning, setShowLoopbackWarning] = useState(false);
|
||||
const [localLoopbackOnly, setLocalLoopbackOnly] = useState(false);
|
||||
const [updateTarget, setUpdateTarget] = useState<string>("app");
|
||||
const [appVersion, setAppVersion] = useState<string>("");
|
||||
const [systemVersion, setSystemVersion] = useState<string>("");
|
||||
const [resetConfig, setResetConfig] = useState(false);
|
||||
const [versionChangeAcknowledged, setVersionChangeAcknowledged] = useState(false);
|
||||
const [customVersionUpdateLoading, setCustomVersionUpdateLoading] = useState(false);
|
||||
const settings = useSettingsStore();
|
||||
const { setDisplayRotation } = useSettingsStore();
|
||||
const [powerSavingEnabled, setPowerSavingEnabled] = useState(false);
|
||||
|
||||
const handleDisplayRotationChange = (rotation: string) => {
|
||||
setDisplayRotation(rotation);
|
||||
handleDisplayRotationSave();
|
||||
};
|
||||
|
||||
const handleDisplayRotationSave = () => {
|
||||
send("setDisplayRotation", { params: { rotation: settings.displayRotation } }, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to set display orientation: ${resp.error.data || "Unknown error"}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
notifications.success("Display orientation updated successfully");
|
||||
});
|
||||
};
|
||||
|
||||
const { setBacklightSettings } = useSettingsStore();
|
||||
|
||||
const handleBacklightSettingsChange = (settings: BacklightSettings) => {
|
||||
// If the user has set the display to dim after it turns off, set the dim_after
|
||||
// value to never.
|
||||
if (settings.dim_after > settings.off_after && settings.off_after != 0) {
|
||||
settings.dim_after = 0;
|
||||
}
|
||||
|
||||
setBacklightSettings(settings);
|
||||
handleBacklightSettingsSave();
|
||||
};
|
||||
|
||||
const handleBacklightSettingsSave = () => {
|
||||
send("setBacklightSettings", { params: settings.backlightSettings }, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to set backlight settings: ${resp.error.data || "Unknown error"}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
notifications.success("Backlight settings updated successfully");
|
||||
});
|
||||
};
|
||||
|
||||
const handlePowerSavingChange = (enabled: boolean) => {
|
||||
setPowerSavingEnabled(enabled);
|
||||
const duration = enabled ? 90 : -1;
|
||||
send("setVideoSleepMode", { duration }, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to set power saving mode: ${resp.error.data || "Unknown error"}`,
|
||||
);
|
||||
setPowerSavingEnabled(!enabled); // Revert on error
|
||||
return;
|
||||
}
|
||||
notifications.success(`Power saving mode ${enabled ? "enabled" : "disabled"}`);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
send("getBacklightSettings", {}, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
return notifications.error(
|
||||
`Failed to get backlight settings: ${resp.error.data || "Unknown error"}`,
|
||||
);
|
||||
}
|
||||
const result = resp.result as BacklightSettings;
|
||||
setBacklightSettings(result);
|
||||
send("getDevModeState", {}, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) return;
|
||||
const result = resp.result as { enabled: boolean };
|
||||
setDeveloperMode(result.enabled);
|
||||
});
|
||||
}, [send, setBacklightSettings]);
|
||||
|
||||
useEffect(() => {
|
||||
send("getVideoSleepMode", {}, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
console.error("Failed to get power saving mode:", resp.error);
|
||||
return;
|
||||
}
|
||||
const result = resp.result as { enabled: boolean; duration: number };
|
||||
setPowerSavingEnabled(result.duration >= 0);
|
||||
send("getSSHKeyState", {}, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) return;
|
||||
setSSHKey(resp.result as string);
|
||||
});
|
||||
|
||||
send("getUsbEmulationState", {}, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) return;
|
||||
setUsbEmulationEnabled(resp.result as boolean);
|
||||
});
|
||||
|
||||
send("getDevChannelState", {}, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) return;
|
||||
setDevChannel(resp.result as boolean);
|
||||
});
|
||||
|
||||
send("getLocalLoopbackOnly", {}, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) return;
|
||||
setLocalLoopbackOnly(resp.result as boolean);
|
||||
});
|
||||
}, [send, setDeveloperMode]);
|
||||
|
||||
const getUsbEmulationState = useCallback(() => {
|
||||
send("getUsbEmulationState", {}, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) return;
|
||||
setUsbEmulationEnabled(resp.result as boolean);
|
||||
});
|
||||
}, [send]);
|
||||
|
||||
const handleUsbEmulationToggle = useCallback(
|
||||
(enabled: boolean) => {
|
||||
send("setUsbEmulationState", { enabled: enabled }, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to ${enabled ? "enable" : "disable"} USB emulation: ${resp.error.data || "Unknown error"}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
setUsbEmulationEnabled(enabled);
|
||||
getUsbEmulationState();
|
||||
});
|
||||
},
|
||||
[getUsbEmulationState, send],
|
||||
);
|
||||
|
||||
const handleResetConfig = useCallback(() => {
|
||||
send("resetConfig", {}, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to reset configuration: ${resp.error.data || "Unknown error"}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
notifications.success("Configuration reset to default successfully");
|
||||
});
|
||||
}, [send]);
|
||||
|
||||
const handleUpdateSSHKey = useCallback(() => {
|
||||
send("setSSHKeyState", { sshKey }, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to update SSH key: ${resp.error.data || "Unknown error"}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
notifications.success("SSH key updated successfully");
|
||||
});
|
||||
}, [send, sshKey]);
|
||||
|
||||
const handleDevModeChange = useCallback(
|
||||
(developerMode: boolean) => {
|
||||
send("setDevModeState", { enabled: developerMode }, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to set dev mode: ${resp.error.data || "Unknown error"}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
setDeveloperMode(developerMode);
|
||||
});
|
||||
},
|
||||
[send, setDeveloperMode],
|
||||
);
|
||||
|
||||
const handleDevChannelChange = useCallback(
|
||||
(enabled: boolean) => {
|
||||
send("setDevChannelState", { enabled }, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to set dev channel state: ${resp.error.data || "Unknown error"}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
setDevChannel(enabled);
|
||||
});
|
||||
},
|
||||
[send, setDevChannel],
|
||||
);
|
||||
|
||||
const applyLoopbackOnlyMode = useCallback(
|
||||
(enabled: boolean) => {
|
||||
send("setLocalLoopbackOnly", { enabled }, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to ${enabled ? "enable" : "disable"} loopback-only mode: ${resp.error.data || "Unknown error"}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
setLocalLoopbackOnly(enabled);
|
||||
if (enabled) {
|
||||
notifications.success(
|
||||
"Loopback-only mode enabled. Restart your device to apply.",
|
||||
);
|
||||
} else {
|
||||
notifications.success(
|
||||
"Loopback-only mode disabled. Restart your device to apply.",
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
[send, setLocalLoopbackOnly],
|
||||
);
|
||||
|
||||
const handleLoopbackOnlyModeChange = useCallback(
|
||||
(enabled: boolean) => {
|
||||
// If trying to enable loopback-only mode, show warning first
|
||||
if (enabled) {
|
||||
setShowLoopbackWarning(true);
|
||||
} else {
|
||||
// If disabling, just proceed
|
||||
applyLoopbackOnlyMode(false);
|
||||
}
|
||||
},
|
||||
[applyLoopbackOnlyMode, setShowLoopbackWarning],
|
||||
);
|
||||
|
||||
const confirmLoopbackModeEnable = useCallback(() => {
|
||||
applyLoopbackOnlyMode(true);
|
||||
setShowLoopbackWarning(false);
|
||||
}, [applyLoopbackOnlyMode, setShowLoopbackWarning]);
|
||||
|
||||
const handleVersionUpdateError = useCallback((error?: JsonRpcError | string) => {
|
||||
notifications.error(
|
||||
m.advanced_error_version_update({
|
||||
error: typeof error === "string" ? error : (error?.data ?? error?.message ?? m.unknown_error())
|
||||
}),
|
||||
{ duration: 1000 * 15 } // 15 seconds
|
||||
);
|
||||
setCustomVersionUpdateLoading(false);
|
||||
}, []);
|
||||
|
||||
const handleCustomVersionUpdate = useCallback(async () => {
|
||||
const components: UpdateComponents = {};
|
||||
if (["app", "both"].includes(updateTarget) && appVersion) components.app = appVersion;
|
||||
if (["system", "both"].includes(updateTarget) && systemVersion) components.system = systemVersion;
|
||||
let versionInfo: SystemVersionInfo | undefined;
|
||||
|
||||
try {
|
||||
// we do not need to set it to false if check succeeds,
|
||||
// because it will be redirected to the update page later
|
||||
setCustomVersionUpdateLoading(true);
|
||||
versionInfo = await checkUpdateComponents({
|
||||
components,
|
||||
}, devChannel);
|
||||
} catch (error: unknown) {
|
||||
const jsonRpcError = error as JsonRpcError;
|
||||
handleVersionUpdateError(jsonRpcError);
|
||||
return;
|
||||
}
|
||||
|
||||
let hasUpdate = false;
|
||||
|
||||
const pageParams = new URLSearchParams();
|
||||
if (components.app && versionInfo?.remote?.appVersion && versionInfo?.appUpdateAvailable) {
|
||||
hasUpdate = true;
|
||||
pageParams.set("custom_app_version", versionInfo.remote?.appVersion);
|
||||
}
|
||||
if (components.system && versionInfo?.remote?.systemVersion && versionInfo?.systemUpdateAvailable) {
|
||||
hasUpdate = true;
|
||||
pageParams.set("custom_system_version", versionInfo.remote?.systemVersion);
|
||||
}
|
||||
pageParams.set("reset_config", resetConfig.toString());
|
||||
|
||||
if (!hasUpdate) {
|
||||
handleVersionUpdateError("No update available");
|
||||
return;
|
||||
}
|
||||
|
||||
// Navigate to update page
|
||||
navigateTo(`/settings/general/update?${pageParams.toString()}`);
|
||||
}, [
|
||||
updateTarget, appVersion, systemVersion, devChannel,
|
||||
navigateTo, resetConfig, handleVersionUpdateError,
|
||||
setCustomVersionUpdateLoading
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<SettingsPageHeader
|
||||
title="Hardware"
|
||||
description="Configure display settings and hardware options for your JetKVM device"
|
||||
title="Advanced"
|
||||
description="Access additional settings for troubleshooting and customization"
|
||||
/>
|
||||
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title="Display Orientation"
|
||||
description="Set the orientation of the display"
|
||||
title="Dev Channel Updates"
|
||||
description="Receive early updates from the development channel"
|
||||
>
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
label=""
|
||||
value={settings.displayRotation.toString()}
|
||||
options={[
|
||||
{ value: "270", label: "Normal" },
|
||||
{ value: "90", label: "Inverted" },
|
||||
]}
|
||||
<Checkbox
|
||||
checked={devChannel}
|
||||
onChange={e => {
|
||||
settings.displayRotation = e.target.value;
|
||||
handleDisplayRotationChange(settings.displayRotation);
|
||||
handleDevChannelChange(e.target.checked);
|
||||
}}
|
||||
/>
|
||||
</SettingsItem>
|
||||
<SettingsItem
|
||||
title="Display Brightness"
|
||||
description="Set the brightness of the display"
|
||||
title="Developer Mode"
|
||||
description="Enable advanced features for developers"
|
||||
>
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
label=""
|
||||
value={settings.backlightSettings.max_brightness.toString()}
|
||||
options={[
|
||||
{ value: "0", label: "Off" },
|
||||
{ value: "10", label: "Low" },
|
||||
{ value: "35", label: "Medium" },
|
||||
{ value: "64", label: "High" },
|
||||
]}
|
||||
<Checkbox
|
||||
checked={settings.developerMode}
|
||||
onChange={e => handleDevModeChange(e.target.checked)}
|
||||
/>
|
||||
</SettingsItem>
|
||||
{settings.developerMode ? (
|
||||
<NestedSettingsGroup>
|
||||
<GridCard>
|
||||
<div className="flex items-start gap-x-4 p-4 select-none">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
className="mt-1 h-8 w-8 shrink-0 text-amber-600 dark:text-amber-500"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M9.401 3.003c1.155-2 4.043-2 5.197 0l7.355 12.748c1.154 2-.29 4.5-2.599 4.5H4.645c-2.309 0-3.752-2.5-2.598-4.5L9.4 3.003zM12 8.25a.75.75 0 01.75.75v3.75a.75.75 0 01-1.5 0V9a.75.75 0 01.75-.75zm0 8.25a.75.75 0 100-1.5.75.75 0 000 1.5z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-base font-bold text-slate-900 dark:text-white">
|
||||
{m.advanced_developer_mode_enabled_title()}
|
||||
</h3>
|
||||
<div>
|
||||
<ul className="list-disc space-y-1 pl-5 text-xs text-slate-700 dark:text-slate-300">
|
||||
<li>{m.advanced_developer_mode_warning_security()}</li>
|
||||
<li>{m.advanced_developer_mode_warning_risks()}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs text-slate-700 dark:text-slate-300">
|
||||
{m.advanced_developer_mode_warning_advanced()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</GridCard>
|
||||
|
||||
{isOnDevice && (
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title={m.advanced_ssh_access_title()}
|
||||
description={m.advanced_ssh_access_description()}
|
||||
/>
|
||||
<TextAreaWithLabel
|
||||
label={m.advanced_ssh_public_key_label()}
|
||||
value={sshKey || ""}
|
||||
rows={3}
|
||||
onChange={e => setSSHKey(e.target.value)}
|
||||
placeholder={m.advanced_ssh_public_key_placeholder()}
|
||||
/>
|
||||
<p className="text-xs text-slate-600 dark:text-slate-400">
|
||||
{m.advanced_ssh_default_user()}<strong>root</strong>.
|
||||
</p>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Button
|
||||
size="SM"
|
||||
theme="primary"
|
||||
text={m.advanced_update_ssh_key_button()}
|
||||
onClick={handleUpdateSSHKey}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<FeatureFlag minAppVersion="0.4.10" name="version-update">
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title={m.advanced_version_update_title()}
|
||||
description={m.advanced_version_update_description()}
|
||||
/>
|
||||
|
||||
<SelectMenuBasic
|
||||
label={m.advanced_version_update_target_label()}
|
||||
options={[
|
||||
{ value: "app", label: m.advanced_version_update_target_app() },
|
||||
{ value: "system", label: m.advanced_version_update_target_system() },
|
||||
{ value: "both", label: m.advanced_version_update_target_both() },
|
||||
]}
|
||||
value={updateTarget}
|
||||
onChange={e => setUpdateTarget(e.target.value)}
|
||||
/>
|
||||
|
||||
{(updateTarget === "app" || updateTarget === "both") && (
|
||||
<InputFieldWithLabel
|
||||
label={m.advanced_version_update_app_label()}
|
||||
placeholder="0.4.9"
|
||||
value={appVersion}
|
||||
onChange={e => setAppVersion(e.target.value)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{(updateTarget === "system" || updateTarget === "both") && (
|
||||
<InputFieldWithLabel
|
||||
label={m.advanced_version_update_system_label()}
|
||||
placeholder="0.4.9"
|
||||
value={systemVersion}
|
||||
onChange={e => setSystemVersion(e.target.value)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<p className="text-xs text-slate-600 dark:text-slate-400">
|
||||
{m.advanced_version_update_helper()}{" "}
|
||||
<a
|
||||
href="https://github.com/jetkvm/kvm/releases"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-medium text-blue-700 hover:underline dark:text-blue-500"
|
||||
>
|
||||
{m.advanced_version_update_github_link()}
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<CheckboxWithLabel
|
||||
label={m.advanced_version_update_reset_config_label()}
|
||||
description={m.advanced_version_update_reset_config_description()}
|
||||
checked={resetConfig}
|
||||
onChange={e => setResetConfig(e.target.checked)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<CheckboxWithLabel
|
||||
label="I understand version changes may break my device and require factory reset"
|
||||
checked={versionChangeAcknowledged}
|
||||
onChange={e => setVersionChangeAcknowledged(e.target.checked)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
size="SM"
|
||||
theme="primary"
|
||||
text={m.advanced_version_update_button()}
|
||||
disabled={
|
||||
(updateTarget === "app" && !appVersion) ||
|
||||
(updateTarget === "system" && !systemVersion) ||
|
||||
(updateTarget === "both" && (!appVersion || !systemVersion)) ||
|
||||
!versionChangeAcknowledged ||
|
||||
customVersionUpdateLoading
|
||||
}
|
||||
loading={customVersionUpdateLoading}
|
||||
onClick={handleCustomVersionUpdate}
|
||||
/>
|
||||
</div>
|
||||
</FeatureFlag>
|
||||
</NestedSettingsGroup>
|
||||
) : null}
|
||||
|
||||
<SettingsItem
|
||||
title={m.advanced_loopback_only_title()}
|
||||
description={m.advanced_loopback_only_description()}
|
||||
>
|
||||
<Checkbox
|
||||
checked={localLoopbackOnly}
|
||||
onChange={e => handleLoopbackOnlyModeChange(e.target.checked)}
|
||||
/>
|
||||
</SettingsItem>
|
||||
|
||||
|
||||
|
||||
<SettingsItem
|
||||
title="Troubleshooting Mode"
|
||||
description="Diagnostic tools and additional controls for troubleshooting and development purposes"
|
||||
>
|
||||
<Checkbox
|
||||
defaultChecked={settings.debugMode}
|
||||
onChange={e => {
|
||||
settings.backlightSettings.max_brightness = parseInt(e.target.value);
|
||||
handleBacklightSettingsChange(settings.backlightSettings);
|
||||
settings.setDebugMode(e.target.checked);
|
||||
}}
|
||||
/>
|
||||
</SettingsItem>
|
||||
{settings.backlightSettings.max_brightness != 0 && (
|
||||
<>
|
||||
|
||||
{settings.debugMode && (
|
||||
<NestedSettingsGroup>
|
||||
<SettingsItem
|
||||
title="Dim Display After"
|
||||
description="Set how long to wait before dimming the display"
|
||||
title="USB Emulation"
|
||||
description="Control the USB emulation state"
|
||||
>
|
||||
<SelectMenuBasic
|
||||
<Button
|
||||
size="SM"
|
||||
label=""
|
||||
value={settings.backlightSettings.dim_after.toString()}
|
||||
options={[
|
||||
{ value: "0", label: "Never" },
|
||||
{ value: "60", label: "1 Minute" },
|
||||
{ value: "300", label: "5 Minutes" },
|
||||
{ value: "600", label: "10 Minutes" },
|
||||
{ value: "1800", label: "30 Minutes" },
|
||||
{ value: "3600", label: "1 Hour" },
|
||||
]}
|
||||
onChange={e => {
|
||||
settings.backlightSettings.dim_after = parseInt(e.target.value);
|
||||
handleBacklightSettingsChange(settings.backlightSettings);
|
||||
}}
|
||||
theme="light"
|
||||
text={
|
||||
usbEmulationEnabled ? "Disable USB Emulation" : "Enable USB Emulation"
|
||||
}
|
||||
onClick={() => handleUsbEmulationToggle(!usbEmulationEnabled)}
|
||||
/>
|
||||
</SettingsItem>
|
||||
|
||||
<SettingsItem
|
||||
title="Turn off Display After"
|
||||
description="Period of inactivity before display automatically turns off"
|
||||
title="Reset Configuration"
|
||||
description="Reset configuration to default. This will log you out."
|
||||
>
|
||||
<SelectMenuBasic
|
||||
<Button
|
||||
size="SM"
|
||||
label=""
|
||||
value={settings.backlightSettings.off_after.toString()}
|
||||
options={[
|
||||
{ value: "0", label: "Never" },
|
||||
{ value: "300", label: "5 Minutes" },
|
||||
{ value: "600", label: "10 Minutes" },
|
||||
{ value: "1800", label: "30 Minutes" },
|
||||
{ value: "3600", label: "1 Hour" },
|
||||
]}
|
||||
onChange={e => {
|
||||
settings.backlightSettings.off_after = parseInt(e.target.value);
|
||||
handleBacklightSettingsChange(settings.backlightSettings);
|
||||
theme="light"
|
||||
text="Reset Config"
|
||||
onClick={() => {
|
||||
handleResetConfig();
|
||||
window.location.reload();
|
||||
}}
|
||||
/>
|
||||
</SettingsItem>
|
||||
</NestedSettingsGroup>
|
||||
)}
|
||||
<p className="text-xs text-slate-600 dark:text-slate-400">
|
||||
The display will wake up when the connection state changes, or when touched.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<FeatureFlag minAppVersion="0.4.9">
|
||||
<div className="space-y-4">
|
||||
<div className="h-px w-full bg-slate-800/10 dark:bg-slate-300/20" />
|
||||
<SettingsSectionHeader
|
||||
title="Power Saving"
|
||||
description="Reduce power consumption when not in use"
|
||||
/>
|
||||
<SettingsItem
|
||||
badge="Experimental"
|
||||
title="HDMI Sleep Mode"
|
||||
description="Turn off capture after 90 seconds of inactivity"
|
||||
>
|
||||
<Checkbox
|
||||
checked={powerSavingEnabled}
|
||||
onChange={(e) => handlePowerSavingChange(e.target.checked)}
|
||||
/>
|
||||
</SettingsItem>
|
||||
</div>
|
||||
</FeatureFlag>
|
||||
|
||||
<FeatureFlag minAppVersion="0.3.8">
|
||||
<UsbDeviceSetting />
|
||||
</FeatureFlag>
|
||||
|
||||
<FeatureFlag minAppVersion="0.3.8">
|
||||
<UsbInfoSetting />
|
||||
</FeatureFlag>
|
||||
<ConfirmDialog
|
||||
open={showLoopbackWarning}
|
||||
onClose={() => {
|
||||
setShowLoopbackWarning(false);
|
||||
}}
|
||||
title="Enable Loopback-Only Mode?"
|
||||
description={
|
||||
<>
|
||||
<p>
|
||||
WARNING: This will restrict web interface access to localhost (127.0.0.1)
|
||||
only.
|
||||
</p>
|
||||
<p>Before enabling this feature, make sure you have either:</p>
|
||||
<ul className="list-disc space-y-1 pl-5 text-xs text-slate-700 dark:text-slate-300">
|
||||
<li>SSH access configured and tested</li>
|
||||
<li>Cloud access enabled and working</li>
|
||||
</ul>
|
||||
</>
|
||||
}
|
||||
variant="warning"
|
||||
confirmText="I Understand, Enable Anyway"
|
||||
onConfirm={confirmLoopbackModeEnable}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
import { KeySequence } from "@hooks/stores";
|
||||
|
||||
export const formatters = {
|
||||
date: (date: Date, options?: Intl.DateTimeFormatOptions) =>
|
||||
new Intl.DateTimeFormat("en-US", {
|
||||
|
|
@ -37,6 +39,7 @@ export const formatters = {
|
|||
...(options || {}),
|
||||
});
|
||||
|
||||
// Note, do not translate the unit names in DIVISIONS, as they must match Intl.RelativeTimeFormatUnit
|
||||
const DIVISIONS: {
|
||||
amount: number;
|
||||
name: Intl.RelativeTimeFormatUnit;
|
||||
|
|
@ -243,3 +246,22 @@ export function isChromeOS() {
|
|||
/* ChromeOS sets navigator.platform to Linux :/ */
|
||||
return !!navigator.userAgent.match(" CrOS ");
|
||||
}
|
||||
|
||||
export function normalizeSortOrders(macros: KeySequence[]): KeySequence[] {
|
||||
return macros.map((macro, index) => ({
|
||||
...macro,
|
||||
sortOrder: index + 1,
|
||||
}));
|
||||
}
|
||||
|
||||
export function deleteCookie(name: string, domain?: string, path = "/") {
|
||||
const domainPart = domain ? `; domain=${domain}` : "";
|
||||
// max-age=0 removes the cookie immediately in modern browsers
|
||||
document.cookie = `${name}=; path=${path}; max-age=0${domainPart}`;
|
||||
// fallback: set an expires in the past for older agents
|
||||
document.cookie = `${name}=; path=${path}; expires=Thu, 01 Jan 1970 00:00:00 GMT${domainPart}`;
|
||||
}
|
||||
|
||||
export function sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@
|
|||
/* Import Aliases */
|
||||
"paths": {
|
||||
"@components/*": ["./src/components/*"],
|
||||
"@hooks/*": ["./src/hooks/*"],
|
||||
"@routes/*": ["./src/routes/*"],
|
||||
"@assets/*": ["./src/assets/*"],
|
||||
"@/*": ["./src/*"]
|
||||
|
|
|
|||
Loading…
Reference in New Issue