From 161c272099e51cf77d41c3a80de62bce569f50dd Mon Sep 17 00:00:00 2001 From: Siyuan Miao Date: Wed, 19 Nov 2025 15:49:00 +0000 Subject: [PATCH] fix: remove localization from 0.4.10 --- go.mod | 11 +- go.sum | 40 +- hw.go | 11 +- scripts/dev_deploy.sh | 123 ++-- ui/src/hooks/stores.ts | 16 +- ui/src/hooks/useVersion.tsx | 91 +-- .../devices.$id.settings.access._index.tsx | 170 +++-- .../devices.$id.settings.general.update.tsx | 248 +++---- .../routes/devices.$id.settings.hardware.tsx | 630 +++++++++++++----- ui/src/utils.ts | 22 + ui/tsconfig.json | 1 + 11 files changed, 809 insertions(+), 554 deletions(-) diff --git a/go.mod b/go.mod index 3e032172..db82df96 100644 --- a/go.mod +++ b/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 ) diff --git a/go.sum b/go.sum index 64b290c7..3fc4ed1a 100644 --- a/go.sum +++ b/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= diff --git a/hw.go b/hw.go index f1670262..2895beaa 100644 --- a/hw.go +++ b/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 diff --git a/scripts/dev_deploy.sh b/scripts/dev_deploy.sh index 0d9c6c0b..6c8b204c 100755 --- a/scripts/dev_deploy.sh +++ b/scripts/dev_deploy.sh @@ -12,14 +12,12 @@ show_help() { echo echo "Optional:" echo " -u, --user Remote username (default: root)" - echo " --gdb-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}" diff --git a/ui/src/hooks/stores.ts b/ui/src/hooks/stores.ts index 9fd721c8..e96f804c 100644 --- a/ui/src/hooks/stores.ts +++ b/ui/src/hooks/stores.ts @@ -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(set => ({ @@ -610,11 +617,16 @@ export const useUpdateStore = create(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"; diff --git a/ui/src/hooks/useVersion.tsx b/ui/src/hooks/useVersion.tsx index 759782e0..d9ce4011 100644 --- a/ui/src/hooks/useVersion.tsx +++ b/ui/src/hooks/useVersion.tsx @@ -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((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((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, }; -} \ No newline at end of file +} diff --git a/ui/src/routes/devices.$id.settings.access._index.tsx b/ui/src/routes/devices.$id.settings.access._index.tsx index a9404470..9b2d3cd3 100644 --- a/ui/src/routes/devices.$id.settings.access._index.tsx +++ b/ui/src/routes/devices.$id.settings.access._index.tsx @@ -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 (
{loaderData?.authMode && ( <>
<> 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() }, ]} /> {tlsMode === "custom" && ( -
-
- -
- handleTlsCertChange(e.target.value)} - /> -
- -
-
- handleTlsKeyChange(e.target.value)} - /> -
-
-
+ + + handleTlsCertChange(e.target.value)} + /> + handleTlsKeyChange(e.target.value)} + />
@@ -282,14 +274,14 @@ export default function SettingsAccessIndexRoute() { )} {loaderData.authMode === "password" ? (
diff --git a/ui/src/routes/devices.$id.settings.hardware.tsx b/ui/src/routes/devices.$id.settings.hardware.tsx index 8f3dcf31..5c625bb5 100644 --- a/ui/src/routes/devices.$id.settings.hardware.tsx +++ b/ui/src/routes/devices.$id.settings.hardware.tsx @@ -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(""); + 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("app"); + const [appVersion, setAppVersion] = useState(""); + const [systemVersion, setSystemVersion] = useState(""); + 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 (
+
- { - settings.displayRotation = e.target.value; - handleDisplayRotationChange(settings.displayRotation); + handleDevChannelChange(e.target.checked); }} /> - handleDevModeChange(e.target.checked)} + /> + + {settings.developerMode ? ( + + +
+ + + +
+
+

+ {m.advanced_developer_mode_enabled_title()} +

+
+
    +
  • {m.advanced_developer_mode_warning_security()}
  • +
  • {m.advanced_developer_mode_warning_risks()}
  • +
+
+
+
+ {m.advanced_developer_mode_warning_advanced()} +
+
+
+
+ + {isOnDevice && ( +
+ + setSSHKey(e.target.value)} + placeholder={m.advanced_ssh_public_key_placeholder()} + /> +

+ {m.advanced_ssh_default_user()}root. +

+
+
+
+ )} + + +
+ + + setUpdateTarget(e.target.value)} + /> + + {(updateTarget === "app" || updateTarget === "both") && ( + setAppVersion(e.target.value)} + /> + )} + + {(updateTarget === "system" || updateTarget === "both") && ( + setSystemVersion(e.target.value)} + /> + )} + +

+ {m.advanced_version_update_helper()}{" "} + + {m.advanced_version_update_github_link()} + +

+ +
+ setResetConfig(e.target.checked)} + /> +
+ +
+ setVersionChangeAcknowledged(e.target.checked)} + /> +
+ +
+
+
+ ) : null} + + + handleLoopbackOnlyModeChange(e.target.checked)} + /> + + + + + + { - settings.backlightSettings.max_brightness = parseInt(e.target.value); - handleBacklightSettingsChange(settings.backlightSettings); + settings.setDebugMode(e.target.checked); }} /> - {settings.backlightSettings.max_brightness != 0 && ( - <> + + {settings.debugMode && ( + - { - settings.backlightSettings.dim_after = parseInt(e.target.value); - handleBacklightSettingsChange(settings.backlightSettings); - }} + theme="light" + text={ + usbEmulationEnabled ? "Disable USB Emulation" : "Enable USB Emulation" + } + onClick={() => handleUsbEmulationToggle(!usbEmulationEnabled)} /> + - { - settings.backlightSettings.off_after = parseInt(e.target.value); - handleBacklightSettingsChange(settings.backlightSettings); + theme="light" + text="Reset Config" + onClick={() => { + handleResetConfig(); + window.location.reload(); }} /> )} -

- The display will wake up when the connection state changes, or when touched. -

- -
-
- - - handlePowerSavingChange(e.target.checked)} - /> - -
- - - - - - - - - + { + setShowLoopbackWarning(false); + }} + title="Enable Loopback-Only Mode?" + description={ + <> +

+ WARNING: This will restrict web interface access to localhost (127.0.0.1) + only. +

+

Before enabling this feature, make sure you have either:

+
    +
  • SSH access configured and tested
  • +
  • Cloud access enabled and working
  • +
+ + } + variant="warning" + confirmText="I Understand, Enable Anyway" + onConfirm={confirmLoopbackModeEnable} + />
); -} +} \ No newline at end of file diff --git a/ui/src/utils.ts b/ui/src/utils.ts index 9e361339..6617d01b 100644 --- a/ui/src/utils.ts +++ b/ui/src/utils.ts @@ -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 { + return new Promise(resolve => setTimeout(resolve, ms)); +} diff --git a/ui/tsconfig.json b/ui/tsconfig.json index 8d4317b1..301b74aa 100644 --- a/ui/tsconfig.json +++ b/ui/tsconfig.json @@ -22,6 +22,7 @@ /* Import Aliases */ "paths": { "@components/*": ["./src/components/*"], + "@hooks/*": ["./src/hooks/*"], "@routes/*": ["./src/routes/*"], "@assets/*": ["./src/assets/*"], "@/*": ["./src/*"]