Compare commits

...

23 Commits

Author SHA1 Message Date
Marc Brooks fe91742c2b
Extend retries to 2000
(which is 20000 seconds after backing off)
2025-10-07 12:04:44 -05:00
Marc Brooks c61ba677e2
Removed duplicate code between main and devices.$id
The isOneDevice and checkAuth can be leveraged from devices.$id.tsx
2025-10-07 12:04:44 -05:00
Marc Brooks 90621a2fec
Fix link for reconnect to point to settings 2025-10-07 12:04:44 -05:00
Marc Brooks 5b0c27b696
Added force page reload to the onClose events of update/reboot
Updated the text about reboot/update and used a smaller button.
Ensure we get the correct UI version.
Also fixed comment about the system update progress
2025-10-07 12:04:43 -05:00
Marc Brooks 7290171c35
Fix comment 2025-10-07 12:04:43 -05:00
Marc Brooks e161339dcf
Added exponential backoff to reconnection
Also made the number of reconnect attempts settable
Doesn't attempt a reconnection if we intentionally disconnect
Make sure the fire-and-forget for TURN activity doesn't result in unhandled promise rejection.
2025-10-07 12:04:43 -05:00
Marc Brooks ad9f079b2b
Add ability to request a reload to LinkButton and Link 2025-10-07 12:04:42 -05:00
Marc Brooks ec626f9d3c
Better reporting of and process for OTA updating 2025-10-07 12:04:42 -05:00
Marc Brooks b144d9926f
Remove the temporary directory after extracting buildkit (#874) 2025-10-07 11:57:26 +02:00
Aylen e755a6e1b1
Update openSUSE image reference to Leap 16.0 (#865) 2025-10-07 11:57:10 +02:00
Marc Brooks 99a8c2711c
Add podman support (#875)
Reimplement #141 since we've changed everything since
2025-10-07 11:43:25 +02:00
Marc Brooks 317218a682
docs: debugging UI builds because of ui symlink (#873)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-04 12:05:14 +02:00
Aveline 62b8dee170
chore: downgrade gin to v1.10.1 (#869) 2025-10-03 08:48:51 +02:00
Alex bdd6f4247b
fix: segfault in cGo 2025-10-02 19:15:03 +02:00
dependabot[bot] 63aa940f42
build(deps): bump github.com/prometheus/client_golang (#851)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-01 21:44:24 +02:00
dependabot[bot] 043ef9ddfc
build(deps): bump github.com/gin-gonic/gin from 1.10.1 to 1.11.0 (#852)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-01 21:44:08 +02:00
Marc Brooks 437f0b854a
upgrade ui packages (#861) 2025-10-01 21:43:46 +02:00
dependabot[bot] a45d55123c
build(deps): bump github.com/prometheus/common from 0.66.0 to 0.66.1 (#855)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-01 21:42:08 +02:00
dependabot[bot] 213e750e04
build(deps): bump github.com/coder/websocket from 1.8.13 to 1.8.14 (#854)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-01 21:41:48 +02:00
dependabot[bot] 6dcb0286e3
build(deps): bump golang.org/x/net from 0.43.0 to 0.44.0 (#856)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-01 21:40:57 +02:00
dependabot[bot] 74ccca0b1a
build(deps): bump golang.org/x/crypto from 0.41.0 to 0.42.0 (#849)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-01 21:39:31 +02:00
dependabot[bot] 0ad435475b
build(deps): bump github.com/go-co-op/gocron/v2 from 2.16.5 to 2.16.6 (#859)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-01 21:39:15 +02:00
dependabot[bot] 23bf3978fa
build(deps): bump actions/setup-go from 5 to 6 (#848)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-01 21:35:35 +02:00
23 changed files with 648 additions and 503 deletions

View File

@ -1,5 +1,5 @@
{ {
"name": "JetKVM", "name": "JetKVM docker devcontainer",
"image": "mcr.microsoft.com/devcontainers/go:1.25-trixie", "image": "mcr.microsoft.com/devcontainers/go:1.25-trixie",
"features": { "features": {
"ghcr.io/devcontainers/features/node:1": { "ghcr.io/devcontainers/features/node:1": {

View File

@ -32,4 +32,5 @@ wget https://github.com/jetkvm/rv1106-system/releases/download/${BUILDKIT_VERSIO
sudo mkdir -p /opt/jetkvm-native-buildkit && \ sudo mkdir -p /opt/jetkvm-native-buildkit && \
sudo tar --use-compress-program="unzstd --long=31" -xvf buildkit.tar.zst -C /opt/jetkvm-native-buildkit && \ sudo tar --use-compress-program="unzstd --long=31" -xvf buildkit.tar.zst -C /opt/jetkvm-native-buildkit && \
rm buildkit.tar.zst rm buildkit.tar.zst
popd popd
rm -rf "${BUILDKIT_TMPDIR}"

View File

@ -0,0 +1,19 @@
{
"name": "JetKVM podman devcontainer",
"image": "mcr.microsoft.com/devcontainers/go:1.25-trixie",
"features": {
"ghcr.io/devcontainers/features/node:1": {
// Should match what is defined in ui/package.json
"version": "22.19.0"
}
},
"runArgs": [
"--userns=keep-id",
"--security-opt=label=disable",
"--security-opt=label=nested"
],
"containerUser": "vscode",
"containerEnv": {
"HOME": "/home/vscode"
}
}

View File

@ -43,7 +43,7 @@ jobs:
cache: "npm" cache: "npm"
cache-dependency-path: "**/package-lock.json" cache-dependency-path: "**/package-lock.json"
- name: Set up Golang - name: Set up Golang
uses: actions/setup-go@v6.0.0 uses: actions/setup-go@v6
with: with:
go-version: "^1.25.1" go-version: "^1.25.1"
- name: Build frontend - name: Build frontend

View File

@ -24,7 +24,7 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v5 uses: actions/checkout@v5
- name: Install Go - name: Install Go
uses: actions/setup-go@v5 uses: actions/setup-go@v6
with: with:
go-version: oldstable go-version: oldstable
- name: Create empty resource directory - name: Create empty resource directory

View File

@ -104,7 +104,7 @@ jobs:
EOF EOF
ssh jkci "cat /tmp/device-tests.json" > device-tests.json ssh jkci "cat /tmp/device-tests.json" > device-tests.json
- name: Set up Golang - name: Set up Golang
uses: actions/setup-go@v5.5.0 uses: actions/setup-go@v6
with: with:
go-version: "1.24.4" go-version: "1.24.4"
- name: Golang Test Report - name: Golang Test Report

View File

@ -97,21 +97,38 @@ tail -f /var/log/jetkvm.log
``` ```
/kvm/ /kvm/
├── main.go # App entry point ├── main.go # App entry point
├── config.go # Settings & configuration ├── config.go # Settings & configuration
├── web.go # API endpoints ├── display.go # Device UI control
├── ui/ # React frontend ├── web.go # API endpoints
│ ├── src/routes/ # Pages (login, settings, etc.) ├── cmd/ # Command line main
│ └── src/components/ # UI components ├── internal/ # Internal Go packages
├── internal/ # Internal Go packages │ ├── confparser/ # Configuration file implementation
│ ├── native/ # CGO / Native code glue layer │ ├── hidrpc/ # HIDRPC implementation for HID devices (keyboard, mouse, etc.)
│ ├── native/cgo/ # C files for the native library (HDMI, Touchscreen, etc.) │ ├── logging/ # Logging implementation
│ ├── native/eez/ # EEZ Studio Project files (for Touchscreen) │ ├── mdns/ # mDNS implementation
│ ├── hidrpc/ # HIDRPC implementation for HID devices (keyboard, mouse, etc.) │ ├── native/ # CGO / Native code glue layer (on-device hardware)
│ ├── logging/ # Logging implementation │ │ ├── cgo/ # C files for the native library (HDMI, Touchscreen, etc.)
│ ├── usbgadget/ # USB gadget │ │ └── eez/ # EEZ Studio Project files (for Touchscreen)
│ └── websecurity/ # TLS certificate management │ ├── network/ # Network implementation
└── resource # netboot iso and other resources │ ├── timesync/ # Time sync/NTP implementation
│ ├── tzdata/ # Timezone data and generation
│ ├── udhcpc/ # DHCP implementation
│ ├── usbgadget/ # USB gadget
│ ├── utils/ # SSH handling
│ └── websecure/ # TLS certificate management
├── resource/ # netboot iso and other resources
├── scripts/ # Bash shell scripts for building and deploying
└── static/ # (react client build output)
└── ui/ # React frontend
├── public/ # UI website static images and fonts
└── src/ # Client React UI
├── assets/ # UI in-page images
├── components/ # UI components
├── hooks/ # Hooks (stores, RPC handling, virtual devices)
├── keyboardLayouts/ # Keyboard layout definitions
├── providers/ # Feature flags
└── routes/ # Pages (login, settings, etc.)
``` ```
**Key files for beginners:** **Key files for beginners:**
@ -252,6 +269,47 @@ rm -rf node_modules
npm install npm install
``` ```
### "Device UI Fails to Build"
If while trying to build you run into an error message similar to :
```plaintext
In file included from /workspaces/kvm/internal/native/cgo/ctrl.c:15:
/workspaces/kvm/internal/native/cgo/ui_index.h:4:10: fatal error: ui/ui.h: No such file or directory
#include "ui/ui.h"
^~~~~~~~~
compilation terminated.
```
This means that your system didn't create the directory-link to from _./internal/native/cgo/ui_ to ./internal/native/eez/src/ui when the repository was checked out. You can verify this is the case if _./internal/native/cgo/ui_ appears as a plain text file with only the textual contents:
```plaintext
../eez/src/ui
```
If this happens to you need to [enable git creation of symbolic links](https://stackoverflow.com/a/59761201/2076) either globally or for the KVM repository:
```bash
# Globally enable git to create symlinks
git config --global core.symlinks true
git restore internal/native/cgo/ui
```
```bash
# Enable git to create symlinks only in this project
git config core.symlinks true
git restore internal/native/cgo/ui
```
Or if you want to manually create the symlink use:
```bash
# linux
cd internal/native/cgo
rm ui
ln -s ../eez/src/ui ui
```
```dos
rem Windows
cd internal/native/cgo
del ui
mklink /d ui ..\eez\src\ui
```
--- ---
## Next Steps ## Next Steps

39
go.mod
View File

@ -5,13 +5,14 @@ go 1.24.4
require ( require (
github.com/Masterminds/semver/v3 v3.4.0 github.com/Masterminds/semver/v3 v3.4.0
github.com/beevik/ntp v1.4.3 github.com/beevik/ntp v1.4.3
github.com/coder/websocket v1.8.13 github.com/coder/websocket v1.8.14
github.com/coreos/go-oidc/v3 v3.15.0 github.com/coreos/go-oidc/v3 v3.15.0
github.com/creack/pty v1.1.24 github.com/creack/pty v1.1.24
github.com/erikdubbelboer/gspt v0.0.0-20210805194459-ce36a5128377
github.com/fsnotify/fsnotify v1.9.0 github.com/fsnotify/fsnotify v1.9.0
github.com/gin-contrib/logger v1.2.6 github.com/gin-contrib/logger v1.2.6
github.com/gin-gonic/gin v1.10.1 github.com/gin-gonic/gin v1.10.1
github.com/go-co-op/gocron/v2 v2.16.5 github.com/go-co-op/gocron/v2 v2.16.6
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/guregu/null/v6 v6.0.0 github.com/guregu/null/v6 v6.0.0
github.com/gwatts/rootcerts v0.0.0-20250901182336-dc5ae18bd79f github.com/gwatts/rootcerts v0.0.0-20250901182336-dc5ae18bd79f
@ -19,8 +20,8 @@ require (
github.com/pion/mdns/v2 v2.0.7 github.com/pion/mdns/v2 v2.0.7
github.com/pion/webrtc/v4 v4.1.4 github.com/pion/webrtc/v4 v4.1.4
github.com/pojntfx/go-nbd v0.3.2 github.com/pojntfx/go-nbd v0.3.2
github.com/prometheus/client_golang v1.23.0 github.com/prometheus/client_golang v1.23.2
github.com/prometheus/common v0.66.0 github.com/prometheus/common v0.66.1
github.com/prometheus/procfs v0.17.0 github.com/prometheus/procfs v0.17.0
github.com/psanford/httpreadat v0.1.0 github.com/psanford/httpreadat v0.1.0
github.com/rs/xid v1.6.0 github.com/rs/xid v1.6.0
@ -30,37 +31,31 @@ require (
github.com/vearutop/statigz v1.5.0 github.com/vearutop/statigz v1.5.0
github.com/vishvananda/netlink v1.3.1 github.com/vishvananda/netlink v1.3.1
go.bug.st/serial v1.6.4 go.bug.st/serial v1.6.4
golang.org/x/crypto v0.41.0 golang.org/x/crypto v0.42.0
golang.org/x/net v0.43.0 golang.org/x/net v0.44.0
golang.org/x/sys v0.35.0 golang.org/x/sys v0.36.0
) )
replace github.com/pojntfx/go-nbd v0.3.2 => github.com/chemhack/go-nbd v0.0.0-20241006125820-59e45f5b1e7b replace github.com/pojntfx/go-nbd v0.3.2 => github.com/chemhack/go-nbd v0.0.0-20241006125820-59e45f5b1e7b
require ( require (
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/bytedance/sonic v1.13.3 // indirect github.com/bytedance/sonic v1.14.0 // indirect
github.com/bytedance/sonic/loader v0.2.4 // indirect github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect github.com/cloudwego/base64x v0.1.6 // indirect
github.com/creack/goselect v0.1.2 // indirect github.com/creack/goselect v0.1.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/erikdubbelboer/gspt v0.0.0-20210805194459-ce36a5128377 // indirect
github.com/gabriel-vasile/mimetype v1.4.9 // indirect github.com/gabriel-vasile/mimetype v1.4.9 // indirect
github.com/gin-contrib/sse v1.1.0 // 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.0 // indirect
github.com/go-ole/go-ole v1.2.4 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.26.0 // indirect github.com/go-playground/validator/v10 v10.27.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-json v0.10.5 // indirect
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect
github.com/jonboulle/clockwork v0.5.0 // indirect github.com/jonboulle/clockwork v0.5.0 // indirect
github.com/jpillora/overseer v1.1.6 // indirect
github.com/jpillora/s3 v1.1.4 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
@ -90,10 +85,10 @@ require (
github.com/ugorji/go/codec v1.3.0 // indirect github.com/ugorji/go/codec v1.3.0 // indirect
github.com/vishvananda/netns v0.0.5 // indirect github.com/vishvananda/netns v0.0.5 // indirect
github.com/wlynxg/anet v0.0.5 // indirect github.com/wlynxg/anet v0.0.5 // indirect
golang.org/x/arch v0.18.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect
golang.org/x/arch v0.20.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/text v0.28.0 // indirect golang.org/x/text v0.29.0 // indirect
google.golang.org/protobuf v1.36.8 // indirect google.golang.org/protobuf v1.36.9 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

86
go.sum
View File

@ -1,7 +1,5 @@
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d h1:G0m3OIz70MZUWq3EgK3CesDbo8upS2Vm9/P3FtgI+Jk=
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/beevik/ntp v1.4.3 h1:PlbTvE5NNy4QHmA4Mg57n7mcFTmr1W1j3gcK7L1lqho= github.com/beevik/ntp v1.4.3 h1:PlbTvE5NNy4QHmA4Mg57n7mcFTmr1W1j3gcK7L1lqho=
@ -10,20 +8,18 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bool64/dev v0.2.39 h1:kP8DnMGlWXhGYJEZE/J0l/gVBdbuhoPGL+MJG4QbofE= github.com/bool64/dev v0.2.39 h1:kP8DnMGlWXhGYJEZE/J0l/gVBdbuhoPGL+MJG4QbofE=
github.com/bool64/dev v0.2.39/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg= github.com/bool64/dev v0.2.39/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg=
github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0= github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY= github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chemhack/go-nbd v0.0.0-20241006125820-59e45f5b1e7b h1:dSbDgy72Y1sjLPWLv7vs0fMFuhMBMViiT9PJZiZWZNs= github.com/chemhack/go-nbd v0.0.0-20241006125820-59e45f5b1e7b h1:dSbDgy72Y1sjLPWLv7vs0fMFuhMBMViiT9PJZiZWZNs=
github.com/chemhack/go-nbd v0.0.0-20241006125820-59e45f5b1e7b/go.mod h1:SehHnbi2e8NiSAKby42Itm8SIoS7b+wAprsfPH3qgYk= github.com/chemhack/go-nbd v0.0.0-20241006125820-59e45f5b1e7b/go.mod h1:SehHnbi2e8NiSAKby42Itm8SIoS7b+wAprsfPH3qgYk=
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE= github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
github.com/coreos/go-oidc/v3 v3.15.0 h1:R6Oz8Z4bqWR7VFQ+sPSvZPQv4x8M+sJkDO5ojgwlyAg= github.com/coreos/go-oidc/v3 v3.15.0 h1:R6Oz8Z4bqWR7VFQ+sPSvZPQv4x8M+sJkDO5ojgwlyAg=
github.com/coreos/go-oidc/v3 v3.15.0/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU= github.com/coreos/go-oidc/v3 v3.15.0/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
@ -46,20 +42,18 @@ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ= 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/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-co-op/gocron/v2 v2.16.5 h1:j228Jxk7bb9CF8LKR3gS+bK3rcjRUINjlVI+ZMp26Ss= github.com/go-co-op/gocron/v2 v2.16.6 h1:zI2Ya9sqvuLcgqJgV79LwoJXM8h20Z/drtB7ATbpRWo=
github.com/go-co-op/gocron/v2 v2.16.5/go.mod h1:zAfC/GFQ668qHxOVl/D68Jh5Ce7sDqX6TJnSQyRkRBc= 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 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.0/go.mod h1:GG/vqmYm3Von2nYiB2vGTXzdoNKE5tix5tuc6iAd+sw=
github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI=
github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 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/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k= github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= 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/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/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
@ -68,26 +62,18 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248=
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk=
github.com/guregu/null/v6 v6.0.0 h1:N14VRS+4di81i1PXRiprbQJ9EM9gqBa0+KVMeS/QSjQ= github.com/guregu/null/v6 v6.0.0 h1:N14VRS+4di81i1PXRiprbQJ9EM9gqBa0+KVMeS/QSjQ=
github.com/guregu/null/v6 v6.0.0/go.mod h1:hrMIhIfrOZeLPZhROSn149tpw2gHkidAqxoXNyeX3iQ= github.com/guregu/null/v6 v6.0.0/go.mod h1:hrMIhIfrOZeLPZhROSn149tpw2gHkidAqxoXNyeX3iQ=
github.com/gwatts/rootcerts v0.0.0-20250901182336-dc5ae18bd79f h1:08t2PbrkDgW2+mwCQ3jhKUBrCM9Bc9SeH5j2Dst3B+0= github.com/gwatts/rootcerts v0.0.0-20250901182336-dc5ae18bd79f h1:08t2PbrkDgW2+mwCQ3jhKUBrCM9Bc9SeH5j2Dst3B+0=
github.com/gwatts/rootcerts v0.0.0-20250901182336-dc5ae18bd79f/go.mod h1:5Kt9XkWvkGi2OHOq0QsGxebHmhCcqJ8KCbNg/a6+n+g= github.com/gwatts/rootcerts v0.0.0-20250901182336-dc5ae18bd79f/go.mod h1:5Kt9XkWvkGi2OHOq0QsGxebHmhCcqJ8KCbNg/a6+n+g=
github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I= github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60= github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
github.com/jpillora/overseer v1.1.6 h1:3ygYfNcR3FfOr22miu3vR1iQcXKMHbmULBh98rbkIyo=
github.com/jpillora/overseer v1.1.6/go.mod h1:aPXQtxuVb9PVWRWTXpo+LdnC/YXQ0IBLNXqKMJmgk88=
github.com/jpillora/s3 v1.1.4 h1:YCCKDWzb/Ye9EBNd83ATRF/8wPEy0xd43Rezb6u6fzc=
github.com/jpillora/s3 v1.1.4/go.mod h1:yedE603V+crlFi1Kl/5vZJaBu9pUzE9wvKegU/lF2zs=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
@ -152,12 +138,12 @@ github.com/pion/webrtc/v4 v4.1.4/go.mod h1:Oab9npu1iZtQRMic3K3toYq5zFPvToe/QBw7d
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc= github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE= github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.66.0 h1:K/rJPHrG3+AoQs50r2+0t7zMnMzek2Vbv31OFVsMeVY= github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
github.com/prometheus/common v0.66.0/go.mod h1:Ux6NtV1B4LatamKE63tJBntoxD++xmtI/lK0VtEplN4= github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
github.com/psanford/httpreadat v0.1.0 h1:VleW1HS2zO7/4c7c7zNl33fO6oYACSagjJIyMIwZLUE= github.com/psanford/httpreadat v0.1.0 h1:VleW1HS2zO7/4c7c7zNl33fO6oYACSagjJIyMIwZLUE=
@ -170,15 +156,12 @@ github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/smartystreets/assertions v1.0.1/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM=
github.com/smartystreets/gunit v1.1.3/go.mod h1:EH5qMBab2UclzXUcpR8b93eHsIlp9u+pDQIRp5DZNzQ=
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 h1:VgoRCP1efSCEZIcF2THLQ46+pIBzzgNiaUBe9wEDwYU=
github.com/sourcegraph/tf-dag v0.2.2-0.20250131204052-3e8ff1477b4f/go.mod h1:pzro7BGorij2WgrjEammtrkbo3+xldxo+KaGLGUiD+Q= github.com/sourcegraph/tf-dag v0.2.2-0.20250131204052-3e8ff1477b4f/go.mod h1:pzro7BGorij2WgrjEammtrkbo3+xldxo+KaGLGUiD+Q=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 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.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
@ -200,12 +183,14 @@ go.bug.st/serial v1.6.4 h1:7FmqNPgVp3pu2Jz5PoPtbZ9jJO5gnEnZIvnI1lzve8A=
go.bug.st/serial v1.6.4/go.mod h1:nofMJxTeNVny/m6+KaafC6vJGj3miwQZ6vW4BZUGJPI= go.bug.st/serial v1.6.4/go.mod h1:nofMJxTeNVny/m6+KaafC6vJGj3miwQZ6vW4BZUGJPI=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc= go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= 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 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -213,20 +198,17 @@ 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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.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.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=

View File

@ -118,7 +118,6 @@ func uiInit(rotation uint16) {
defer cgoLock.Unlock() defer cgoLock.Unlock()
cRotation := C.u_int16_t(rotation) cRotation := C.u_int16_t(rotation)
defer C.free(unsafe.Pointer(&cRotation))
C.jetkvm_ui_init(cRotation) C.jetkvm_ui_init(cRotation)
} }
@ -350,7 +349,6 @@ func uiDispSetRotation(rotation uint16) (bool, error) {
nativeLogger.Info().Uint16("rotation", rotation).Msg("setting rotation") nativeLogger.Info().Uint16("rotation", rotation).Msg("setting rotation")
cRotation := C.u_int16_t(rotation) cRotation := C.u_int16_t(rotation)
defer C.free(unsafe.Pointer(&cRotation))
C.jetkvm_ui_set_rotation(cRotation) C.jetkvm_ui_set_rotation(cRotation)
return true, nil return true, nil

27
main.go
View File

@ -14,6 +14,7 @@ import (
var appCtx context.Context var appCtx context.Context
func Main() { func Main() {
logger.Log().Msg("JetKVM Starting Up")
LoadConfig() LoadConfig()
var cancel context.CancelFunc var cancel context.CancelFunc
@ -78,16 +79,16 @@ func Main() {
initDisplay() initDisplay()
go func() { go func() {
// wait for 15 minutes before starting auto-update checks
// this is to avoid interfering with initial setup processes
// and to ensure the system is stable before checking for updates
time.Sleep(15 * time.Minute) time.Sleep(15 * time.Minute)
for {
logger.Debug().Bool("auto_update_enabled", config.AutoUpdateEnabled).Msg("UPDATING")
if !config.AutoUpdateEnabled {
return
}
if isTimeSyncNeeded() || !timeSync.IsSyncSuccess() { for {
logger.Debug().Msg("system time is not synced, will retry in 30 seconds") logger.Info().Bool("auto_update_enabled", config.AutoUpdateEnabled).Msg("auto-update check")
time.Sleep(30 * time.Second) if !config.AutoUpdateEnabled {
logger.Debug().Msg("auto-update disabled")
time.Sleep(5 * time.Minute) // we'll check if auto-updates are enabled in five minutes
continue continue
} }
@ -97,6 +98,12 @@ func Main() {
continue continue
} }
if isTimeSyncNeeded() || !timeSync.IsSyncSuccess() {
logger.Debug().Msg("system time is not synced, will retry in 30 seconds")
time.Sleep(30 * time.Second)
continue
}
includePreRelease := config.IncludePreRelease includePreRelease := config.IncludePreRelease
err = TryUpdate(context.Background(), GetDeviceID(), includePreRelease) err = TryUpdate(context.Background(), GetDeviceID(), includePreRelease)
if err != nil { if err != nil {
@ -106,6 +113,7 @@ func Main() {
time.Sleep(1 * time.Hour) time.Sleep(1 * time.Hour)
} }
}() }()
//go RunFuseServer() //go RunFuseServer()
go RunWebServer() go RunWebServer()
@ -122,7 +130,8 @@ func Main() {
sigs := make(chan os.Signal, 1) sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
<-sigs <-sigs
logger.Info().Msg("JetKVM Shutting Down")
logger.Log().Msg("JetKVM Shutting Down")
//if fuseServer != nil { //if fuseServer != nil {
// err := setMassStorageImage(" ") // err := setMassStorageImage(" ")
// if err != nil { // if err != nil {

47
ota.go
View File

@ -176,7 +176,7 @@ func downloadFile(ctx context.Context, path string, url string, downloadProgress
if nr > 0 { if nr > 0 {
nw, ew := file.Write(buf[0:nr]) nw, ew := file.Write(buf[0:nr])
if nw < nr { if nw < nr {
return fmt.Errorf("short write: %d < %d", nw, nr) return fmt.Errorf("short file write: %d < %d", nw, nr)
} }
written += int64(nw) written += int64(nw)
if ew != nil { if ew != nil {
@ -240,7 +240,7 @@ func verifyFile(path string, expectedHash string, verifyProgress *float32, scope
if nr > 0 { if nr > 0 {
nw, ew := hash.Write(buf[0:nr]) nw, ew := hash.Write(buf[0:nr])
if nw < nr { if nw < nr {
return fmt.Errorf("short write: %d < %d", nw, nr) return fmt.Errorf("short hash write: %d < %d", nw, nr)
} }
verified += int64(nw) verified += int64(nw)
if ew != nil { if ew != nil {
@ -260,11 +260,16 @@ func verifyFile(path string, expectedHash string, verifyProgress *float32, scope
} }
} }
hashSum := hash.Sum(nil) // close the file so we can rename below
scopedLogger.Info().Str("path", path).Str("hash", hex.EncodeToString(hashSum)).Msg("SHA256 hash of") if err := fileToHash.Close(); err != nil {
return fmt.Errorf("error closing file: %w", err)
}
if hex.EncodeToString(hashSum) != expectedHash { hashSum := hex.EncodeToString(hash.Sum(nil))
return fmt.Errorf("hash mismatch: %x != %s", hashSum, expectedHash) scopedLogger.Info().Str("path", path).Str("hash", hashSum).Msg("SHA256 hash of")
if hashSum != expectedHash {
return fmt.Errorf("hash mismatch: %s != %s", hashSum, expectedHash)
} }
if err := os.Rename(unverifiedPath, path); err != nil { if err := os.Rename(unverifiedPath, path); err != nil {
@ -296,6 +301,8 @@ type OTAState struct {
AppUpdatedAt *time.Time `json:"appUpdatedAt,omitempty"` AppUpdatedAt *time.Time `json:"appUpdatedAt,omitempty"`
SystemUpdateProgress float32 `json:"systemUpdateProgress,omitempty"` //TODO: port rk_ota, then implement SystemUpdateProgress float32 `json:"systemUpdateProgress,omitempty"` //TODO: port rk_ota, then implement
SystemUpdatedAt *time.Time `json:"systemUpdatedAt,omitempty"` SystemUpdatedAt *time.Time `json:"systemUpdatedAt,omitempty"`
RebootNeeded bool `json:"rebootNeeded,omitempty"`
Rebooting bool `json:"rebooting,omitempty"`
} }
var otaState = OTAState{} var otaState = OTAState{}
@ -313,7 +320,7 @@ func triggerOTAStateUpdate() {
func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) error { func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) error {
scopedLogger := otaLogger.With(). scopedLogger := otaLogger.With().
Str("deviceId", deviceId). Str("deviceId", deviceId).
Str("includePreRelease", fmt.Sprintf("%v", includePreRelease)). Bool("includePreRelease", includePreRelease).
Logger() Logger()
scopedLogger.Info().Msg("Trying to update...") scopedLogger.Info().Msg("Trying to update...")
@ -322,7 +329,8 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err
} }
otaState = OTAState{ otaState = OTAState{
Updating: true, Updating: true,
RebootNeeded: false,
} }
triggerOTAStateUpdate() triggerOTAStateUpdate()
@ -335,7 +343,7 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err
if err != nil { if err != nil {
otaState.Error = fmt.Sprintf("Error checking for updates: %v", err) otaState.Error = fmt.Sprintf("Error checking for updates: %v", err)
scopedLogger.Error().Err(err).Msg("Error checking for updates") scopedLogger.Error().Err(err).Msg("Error checking for updates")
return fmt.Errorf("error checking for updates: %w", err) return err
} }
now := time.Now() now := time.Now()
@ -349,8 +357,6 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err
appUpdateAvailable := updateStatus.AppUpdateAvailable appUpdateAvailable := updateStatus.AppUpdateAvailable
systemUpdateAvailable := updateStatus.SystemUpdateAvailable systemUpdateAvailable := updateStatus.SystemUpdateAvailable
rebootNeeded := false
if appUpdateAvailable { if appUpdateAvailable {
scopedLogger.Info(). scopedLogger.Info().
Str("local", local.AppVersion). Str("local", local.AppVersion).
@ -361,7 +367,6 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err
if err != nil { if err != nil {
otaState.Error = fmt.Sprintf("Error downloading app update: %v", err) otaState.Error = fmt.Sprintf("Error downloading app update: %v", err)
scopedLogger.Error().Err(err).Msg("Error downloading app update") scopedLogger.Error().Err(err).Msg("Error downloading app update")
triggerOTAStateUpdate()
return err return err
} }
downloadFinished := time.Now() downloadFinished := time.Now()
@ -378,18 +383,20 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err
if err != nil { if err != nil {
otaState.Error = fmt.Sprintf("Error verifying app update hash: %v", err) otaState.Error = fmt.Sprintf("Error verifying app update hash: %v", err)
scopedLogger.Error().Err(err).Msg("Error verifying app update hash") scopedLogger.Error().Err(err).Msg("Error verifying app update hash")
triggerOTAStateUpdate()
return err return err
} }
verifyFinished := time.Now() verifyFinished := time.Now()
otaState.AppVerifiedAt = &verifyFinished otaState.AppVerifiedAt = &verifyFinished
otaState.AppVerificationProgress = 1 otaState.AppVerificationProgress = 1
triggerOTAStateUpdate()
otaState.AppUpdatedAt = &verifyFinished otaState.AppUpdatedAt = &verifyFinished
otaState.AppUpdateProgress = 1 otaState.AppUpdateProgress = 1
triggerOTAStateUpdate() triggerOTAStateUpdate()
scopedLogger.Info().Msg("App update downloaded") scopedLogger.Info().Msg("App update downloaded")
rebootNeeded = true otaState.RebootNeeded = true
triggerOTAStateUpdate()
} else { } else {
scopedLogger.Info().Msg("App is up to date") scopedLogger.Info().Msg("App is up to date")
} }
@ -404,7 +411,6 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err
if err != nil { if err != nil {
otaState.Error = fmt.Sprintf("Error downloading system update: %v", err) otaState.Error = fmt.Sprintf("Error downloading system update: %v", err)
scopedLogger.Error().Err(err).Msg("Error downloading system update") scopedLogger.Error().Err(err).Msg("Error downloading system update")
triggerOTAStateUpdate()
return err return err
} }
downloadFinished := time.Now() downloadFinished := time.Now()
@ -421,7 +427,6 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err
if err != nil { if err != nil {
otaState.Error = fmt.Sprintf("Error verifying system update hash: %v", err) otaState.Error = fmt.Sprintf("Error verifying system update hash: %v", err)
scopedLogger.Error().Err(err).Msg("Error verifying system update hash") scopedLogger.Error().Err(err).Msg("Error verifying system update hash")
triggerOTAStateUpdate()
return err return err
} }
scopedLogger.Info().Msg("System update downloaded") scopedLogger.Info().Msg("System update downloaded")
@ -441,6 +446,7 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err
scopedLogger.Error().Err(err).Msg("Error starting rk_ota command") scopedLogger.Error().Err(err).Msg("Error starting rk_ota command")
return fmt.Errorf("error starting rk_ota command: %w", err) return fmt.Errorf("error starting rk_ota command: %w", err)
} }
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
defer cancel() defer cancel()
@ -481,14 +487,19 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err
otaState.SystemUpdateProgress = 1 otaState.SystemUpdateProgress = 1
otaState.SystemUpdatedAt = &verifyFinished otaState.SystemUpdatedAt = &verifyFinished
triggerOTAStateUpdate() triggerOTAStateUpdate()
rebootNeeded = true
otaState.RebootNeeded = true
triggerOTAStateUpdate()
} else { } else {
scopedLogger.Info().Msg("System is up to date") scopedLogger.Info().Msg("System is up to date")
} }
if rebootNeeded { if otaState.RebootNeeded {
scopedLogger.Info().Msg("System Rebooting in 10s") scopedLogger.Info().Msg("System Rebooting in 10s")
time.Sleep(10 * time.Second) time.Sleep(10 * time.Second)
otaState.Rebooting = true
triggerOTAStateUpdate()
cmd := exec.Command("reboot") cmd := exec.Command("reboot")
err := cmd.Start() err := cmd.Start()
if err != nil { if err != nil {

640
ui/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
{ {
"name": "kvm-ui", "name": "kvm-ui",
"private": true, "private": true,
"version": "2025.09.26.01300", "version": "2025.10.01.1900",
"type": "module", "type": "module",
"engines": { "engines": {
"node": "^22.15.0" "node": "^22.15.0"
@ -42,7 +42,7 @@
"react-hot-toast": "^2.6.0", "react-hot-toast": "^2.6.0",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"react-router": "^7.9.3", "react-router": "^7.9.3",
"react-simple-keyboard": "^3.8.122", "react-simple-keyboard": "^3.8.125",
"react-use-websocket": "^4.13.0", "react-use-websocket": "^4.13.0",
"react-xtermjs": "^1.0.10", "react-xtermjs": "^1.0.10",
"recharts": "^3.2.1", "recharts": "^3.2.1",
@ -56,15 +56,15 @@
"@eslint/eslintrc": "^3.3.1", "@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.36.0", "@eslint/js": "^9.36.0",
"@tailwindcss/forms": "^0.5.10", "@tailwindcss/forms": "^0.5.10",
"@tailwindcss/postcss": "^4.1.13", "@tailwindcss/postcss": "^4.1.14",
"@tailwindcss/typography": "^0.5.19", "@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "^4.1.13", "@tailwindcss/vite": "^4.1.14",
"@types/react": "^19.1.14", "@types/react": "^19.1.17",
"@types/react-dom": "^19.1.9", "@types/react-dom": "^19.1.10",
"@types/semver": "^7.7.1", "@types/semver": "^7.7.1",
"@types/validator": "^13.15.3", "@types/validator": "^13.15.3",
"@typescript-eslint/eslint-plugin": "^8.44.1", "@typescript-eslint/eslint-plugin": "^8.45.0",
"@typescript-eslint/parser": "^8.44.1", "@typescript-eslint/parser": "^8.45.0",
"@vitejs/plugin-react-swc": "^4.1.0", "@vitejs/plugin-react-swc": "^4.1.0",
"autoprefixer": "^10.4.21", "autoprefixer": "^10.4.21",
"eslint": "^9.36.0", "eslint": "^9.36.0",
@ -77,8 +77,8 @@
"postcss": "^8.5.6", "postcss": "^8.5.6",
"prettier": "^3.6.2", "prettier": "^3.6.2",
"prettier-plugin-tailwindcss": "^0.6.14", "prettier-plugin-tailwindcss": "^0.6.14",
"tailwindcss": "^4.1.13", "tailwindcss": "^4.1.14",
"typescript": "^5.9.2", "typescript": "^5.9.3",
"vite": "^7.1.7", "vite": "^7.1.7",
"vite-tsconfig-paths": "^5.1.4" "vite-tsconfig-paths": "^5.1.4"
} }

View File

@ -213,7 +213,7 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonPropsType>(
Button.displayName = "Button"; Button.displayName = "Button";
type LinkPropsType = Pick<LinkProps, "to"> & type LinkPropsType = Pick<LinkProps, "to"> &
React.ComponentProps<typeof ButtonContent> & { disabled?: boolean }; React.ComponentProps<typeof ButtonContent> & { disabled?: boolean, reloadDocument?: boolean };
export const LinkButton = ({ to, ...props }: LinkPropsType) => { export const LinkButton = ({ to, ...props }: LinkPropsType) => {
const classes = cx( const classes = cx(
"group outline-hidden", "group outline-hidden",
@ -231,7 +231,7 @@ export const LinkButton = ({ to, ...props }: LinkPropsType) => {
); );
} else { } else {
return ( return (
<Link to={to} className={classes}> <Link to={to} reloadDocument={props.reloadDocument} className={classes}>
<ButtonContent {...props} /> <ButtonContent {...props} />
</Link> </Link>
); );

View File

@ -518,6 +518,7 @@ export type UpdateModalViews =
| "upToDate" | "upToDate"
| "updateAvailable" | "updateAvailable"
| "updateCompleted" | "updateCompleted"
| "rebooting"
| "error"; | "error";
export interface OtaState { export interface OtaState {
@ -549,19 +550,26 @@ export interface OtaState {
systemUpdateProgress: number; systemUpdateProgress: number;
systemUpdatedAt: string | null; systemUpdatedAt: string | null;
rebootNeeded: boolean;
rebooting: boolean;
}; };
export interface UpdateState { export interface UpdateState {
isUpdatePending: boolean; isUpdatePending: boolean;
setIsUpdatePending: (isPending: boolean) => void; setIsUpdatePending: (isPending: boolean) => void;
updateDialogHasBeenMinimized: boolean; updateDialogHasBeenMinimized: boolean;
setUpdateDialogHasBeenMinimized: (hasBeenMinimized: boolean) => void;
otaState: OtaState; otaState: OtaState;
setOtaState: (state: OtaState) => void; setOtaState: (state: OtaState) => void;
setUpdateDialogHasBeenMinimized: (hasBeenMinimized: boolean) => void;
modalView: UpdateModalViews modalView: UpdateModalViews
setModalView: (view: UpdateModalViews) => void; setModalView: (view: UpdateModalViews) => void;
setUpdateErrorMessage: (errorMessage: string) => void;
updateErrorMessage: string | null; updateErrorMessage: string | null;
setUpdateErrorMessage: (errorMessage: string) => void;
} }
export const useUpdateStore = create<UpdateState>(set => ({ export const useUpdateStore = create<UpdateState>(set => ({
@ -587,13 +595,17 @@ export const useUpdateStore = create<UpdateState>(set => ({
appUpdatedAt: null, appUpdatedAt: null,
systemUpdateProgress: 0, systemUpdateProgress: 0,
systemUpdatedAt: null, systemUpdatedAt: null,
rebootNeeded: false,
rebooting: false,
}, },
updateDialogHasBeenMinimized: false, updateDialogHasBeenMinimized: false,
setUpdateDialogHasBeenMinimized: (hasBeenMinimized: boolean) => setUpdateDialogHasBeenMinimized: (hasBeenMinimized: boolean) =>
set({ updateDialogHasBeenMinimized: hasBeenMinimized }), set({ updateDialogHasBeenMinimized: hasBeenMinimized }),
modalView: "loading", modalView: "loading",
setModalView: (view: UpdateModalViews) => set({ modalView: view }), setModalView: (view: UpdateModalViews) => set({ modalView: view }),
updateErrorMessage: null, updateErrorMessage: null,
setUpdateErrorMessage: (errorMessage: string) => set({ updateErrorMessage: errorMessage }), setUpdateErrorMessage: (errorMessage: string) => set({ updateErrorMessage: errorMessage }),
})); }));

View File

@ -153,13 +153,13 @@ body {
@property --grid-color-start { @property --grid-color-start {
syntax: "<color>"; syntax: "<color>";
initial-value: var(--color-blue-50/10); initial-value: oklch(97% 0.014 254.604 / 10); /* var(--color-blue-50/10) */
inherits: false; inherits: false;
} }
@property --grid-color-end { @property --grid-color-end {
syntax: "<color>"; syntax: "<color>";
initial-value: var(--color-blue-50/100); initial-value: oklch(97% 0.014 254.604 / 100); /* var(--color-blue-50/100) */
inherits: false; inherits: false;
} }
@ -175,8 +175,8 @@ body {
} }
.group:hover .grid-card { .group:hover .grid-card {
--grid-color-start: var(--color-blue-100/50); --grid-color-start: oklch(from var(--color-blue-100) l c h / 50);
--grid-color-end: var(--color-blue-50/50); --grid-color-end: oklch(from var(--color-blue-50) l c h / 50);
} }
video::-webkit-media-controls { video::-webkit-media-controls {

View File

@ -85,7 +85,7 @@ export async function checkDeviceAuth() {
} }
export async function checkAuth() { export async function checkAuth() {
return import.meta.env.MODE === "device" ? checkDeviceAuth() : checkCloudAuth(); return isOnDevice ? checkDeviceAuth() : checkCloudAuth();
} }
let router; let router;

View File

@ -374,8 +374,8 @@ function UrlView({
icon: FedoraIcon, icon: FedoraIcon,
}, },
{ {
name: "openSUSE Leap 15.6", name: "openSUSE Leap 16.0",
url: "https://download.opensuse.org/distribution/leap/15.6/iso/openSUSE-Leap-15.6-NET-x86_64-Media.iso", url: "https://download.opensuse.org/distribution/leap/16.0/offline/Leap-16.0-online-installer-x86_64.install.iso",
icon: OpenSUSEIcon, icon: OpenSUSEIcon,
}, },
{ {

View File

@ -98,7 +98,7 @@ export default function SettingsAccessIndexRoute() {
} }
getCloudState(); 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("/"); if (!isOnDevice) navigate("/");
return; return;
}); });

View File

@ -7,6 +7,12 @@ import { Button } from "@components/Button";
export default function SettingsGeneralRebootRoute() { export default function SettingsGeneralRebootRoute() {
const navigate = useNavigate(); const navigate = useNavigate();
const { send } = useJsonRpc(); const { send } = useJsonRpc();
const onClose = useCallback(() => {
navigate(".."); // back to the devices.$id.settings page
window.location.reload(); // force a full reload to ensure the current device/cloud UI version is loaded
}, [navigate]);
const onConfirmUpdate = useCallback(() => { const onConfirmUpdate = useCallback(() => {
// This is where we send the RPC to the golang binary // This is where we send the RPC to the golang binary
@ -16,7 +22,7 @@ export default function SettingsGeneralRebootRoute() {
{ {
/* TODO: Migrate to using URLs instead of the global state. To simplify the refactoring, we'll keep the global state for now. */ /* TODO: Migrate to using URLs instead of the global state. To simplify the refactoring, we'll keep the global state for now. */
} }
return <Dialog onClose={() => navigate("..")} onConfirmUpdate={onConfirmUpdate} />; return <Dialog onClose={onClose} onConfirmUpdate={onConfirmUpdate} />;
} }
export function Dialog({ export function Dialog({

View File

@ -1,10 +1,11 @@
import { useLocation, useNavigate } from "react-router";
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import { useLocation, useNavigate } from "react-router";
import { CheckCircleIcon } from "@heroicons/react/20/solid"; import { CheckCircleIcon } from "@heroicons/react/20/solid";
import { MdConnectWithoutContact, MdRestartAlt } from "react-icons/md";
import Card from "@/components/Card"; import Card from "@/components/Card";
import { useJsonRpc } from "@/hooks/useJsonRpc"; import { useJsonRpc } from "@/hooks/useJsonRpc";
import { Button } from "@components/Button"; import { Button, LinkButton } from "@components/Button";
import { UpdateState, useUpdateStore } from "@/hooks/stores"; import { UpdateState, useUpdateStore } from "@/hooks/stores";
import LoadingSpinner from "@/components/LoadingSpinner"; import LoadingSpinner from "@/components/LoadingSpinner";
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation"; import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
@ -18,6 +19,11 @@ export default function SettingsGeneralUpdateRoute() {
const { setModalView, otaState } = useUpdateStore(); const { setModalView, otaState } = useUpdateStore();
const { send } = useJsonRpc(); const { send } = useJsonRpc();
const onClose = useCallback(() => {
navigate(".."); // back to the devices.$id.settings page
window.location.reload(); // force a full reload to ensure the current device/cloud UI version is loaded
}, [navigate]);
const onConfirmUpdate = useCallback(() => { const onConfirmUpdate = useCallback(() => {
send("tryUpdate", {}); send("tryUpdate", {});
setModalView("updating"); setModalView("updating");
@ -33,16 +39,14 @@ export default function SettingsGeneralUpdateRoute() {
} else { } else {
setModalView("loading"); setModalView("loading");
} }
}, [otaState.updating, otaState.error, setModalView, updateSuccess]); }, [otaState.error, otaState.updating, setModalView, updateSuccess]);
{ {
/* TODO: Migrate to using URLs instead of the global state. To simplify the refactoring, we'll keep the global state for now. */ /* TODO: Migrate to using URLs instead of the global state. To simplify the refactoring, we'll keep the global state for now. */
} }
return <Dialog onClose={() => navigate("..")} onConfirmUpdate={onConfirmUpdate} />; return <Dialog onClose={onClose} onConfirmUpdate={onConfirmUpdate} />;
} }
export function Dialog({ export function Dialog({
onClose, onClose,
onConfirmUpdate, onConfirmUpdate,
@ -224,7 +228,7 @@ function UpdatingDeviceState({
100, 100,
); );
} else { } else {
// System: 10% download, 90% update // System: 10% download, 10% verification, 80% update
return Math.min( return Math.min(
downloadProgress * 0.1 + verificationProgress * 0.1 + updateProgress * 0.8, downloadProgress * 0.1 + verificationProgress * 0.1 + updateProgress * 0.8,
100, 100,
@ -239,14 +243,18 @@ function UpdatingDeviceState({
if (!otaState.metadataFetchedAt) { if (!otaState.metadataFetchedAt) {
return "Fetching update information..."; return "Fetching update information...";
} else if (otaState.rebooting) {
return "Rebooting...";
} else if (!downloadFinishedAt) { } else if (!downloadFinishedAt) {
return `Downloading ${type} update...`; return `Downloading ${type} update...`;
} else if (!verfiedAt) { } else if (!verfiedAt) {
return `Verifying ${type} update...`; return `Verifying ${type} update...`;
} else if (!updatedAt) { } else if (!updatedAt) {
return `Installing ${type} update...`; return `Installing ${type} update...`;
} else if (otaState.rebootNeeded) {
return "Reboot needed";
} else { } else {
return `Awaiting reboot`; return "Awaiting reboot";
} }
}; };
@ -278,12 +286,51 @@ function UpdatingDeviceState({
<Card className="space-y-4 p-4"> <Card className="space-y-4 p-4">
{areAllUpdatesComplete() ? ( {areAllUpdatesComplete() ? (
<div className="my-2 flex flex-col items-center space-y-2 text-center"> <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" /> <CheckCircleIcon 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"> {otaState.rebooting ? (
<span className="font-medium text-black dark:text-white"> <div className="flex justify-between text-sm text-slate-600 dark:text-slate-300">
Rebooting to complete the update... <span className="font-medium text-black dark:text-white">
</span> Rebooting the device to complete the update...
</div> </span>
<p className="flex-col text-black dark:text-white">
This may take a few minutes. The device will automatically
reconnect once it is back online.<br/>
If it doesn{"'"}t reconnect automatically, you can manually
reconnect by clicking here:
<LinkButton
size="XS"
theme="light"
text="Reconnect to KVM"
LeadingIcon={MdConnectWithoutContact}
textAlign="center"
reloadDocument={true}
to={".."}
/>
</p>
</div>
) : (
otaState.rebootNeeded && (
<div className="flex justify-between text-sm text-slate-600 dark:text-slate-300">
<span className="font-medium text-black dark:text-white">
Device reboot is pending...
</span>
<p className="flex-col text-black dark:text-white">
The JetKVM is preparing to reboot. This may take a while.<br/>
If it doesn{"'"}t automatically reboot after a few minutes, you
can manually request a reboot by clicking here:
<LinkButton
size="XS"
theme="light"
text="Reboot the KVM"
LeadingIcon={MdRestartAlt}
textAlign="center"
reloadDocument={true}
to={"../reboot"}
/>
</p>
</div>
)
)}
</div> </div>
) : ( ) : (
<> <>

View File

@ -1,7 +1,6 @@
import { lazy, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { lazy, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { import {
Outlet, Outlet,
redirect,
useLoaderData, useLoaderData,
useLocation, useLocation,
useNavigate, useNavigate,
@ -15,7 +14,7 @@ import { FocusTrap } from "focus-trap-react";
import { motion, AnimatePresence } from "framer-motion"; import { motion, AnimatePresence } from "framer-motion";
import useWebSocket from "react-use-websocket"; import useWebSocket from "react-use-websocket";
import { CLOUD_API, DEVICE_API } from "@/ui.config"; import { CLOUD_API } from "@/ui.config";
import api from "@/api"; import api from "@/api";
import { checkAuth, isInCloud, isOnDevice } from "@/main"; import { checkAuth, isInCloud, isOnDevice } from "@/main";
import { cx } from "@/cva.config"; import { cx } from "@/cva.config";
@ -48,7 +47,6 @@ import {
} from "@/components/VideoOverlay"; } from "@/components/VideoOverlay";
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation"; import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
import { FeatureFlagProvider } from "@/providers/FeatureFlagProvider"; import { FeatureFlagProvider } from "@/providers/FeatureFlagProvider";
import { DeviceStatus } from "@routes/welcome-local";
import { useVersion } from "@/hooks/useVersion"; import { useVersion } from "@/hooks/useVersion";
interface LocalLoaderResp { interface LocalLoaderResp {
@ -70,20 +68,8 @@ export interface LocalDevice {
} }
const deviceLoader = async () => { const deviceLoader = async () => {
const res = await api const device = await checkAuth();
.GET(`${DEVICE_API}/device/status`) return { authMode: device.authMode } as LocalLoaderResp;
.then(res => res.json() as Promise<DeviceStatus>);
if (!res.isSetup) return redirect("/welcome");
const deviceRes = await api.GET(`${DEVICE_API}/device`);
if (deviceRes.status === 401) return redirect("/login-local");
if (deviceRes.ok) {
const device = (await deviceRes.json()) as LocalDevice;
return { authMode: device.authMode };
}
throw new Error("Error fetching device");
}; };
const cloudLoader = async (params: Params<string>): Promise<CloudLoaderResp> => { const cloudLoader = async (params: Params<string>): Promise<CloudLoaderResp> => {
@ -106,11 +92,11 @@ const cloudLoader = async (params: Params<string>): Promise<CloudLoaderResp> =>
device: { id: string; name: string; user: { googleId: string } }; device: { id: string; name: string; user: { googleId: string } };
}; };
return { user, iceConfig, deviceName: device.name || device.id }; return { user, iceConfig, deviceName: device.name || device.id } as CloudLoaderResp;
}; };
const loader: LoaderFunction = ({ params }: LoaderFunctionArgs) => { const loader: LoaderFunction = ({ params }: LoaderFunctionArgs) => {
return import.meta.env.MODE === "device" ? deviceLoader() : cloudLoader(params); return isOnDevice ? deviceLoader() : cloudLoader(params);
}; };
export default function KvmIdRoute() { export default function KvmIdRoute() {
@ -146,6 +132,7 @@ export default function KvmIdRoute() {
const { otaState, setOtaState, setModalView } = useUpdateStore(); const { otaState, setOtaState, setModalView } = useUpdateStore();
const [loadingMessage, setLoadingMessage] = useState("Connecting to device..."); const [loadingMessage, setLoadingMessage] = useState("Connecting to device...");
const cleanupAndStopReconnecting = useCallback( const cleanupAndStopReconnecting = useCallback(
function cleanupAndStopReconnecting() { function cleanupAndStopReconnecting() {
console.log("Closing peer connection"); console.log("Closing peer connection");
@ -182,11 +169,11 @@ export default function KvmIdRoute() {
pc: RTCPeerConnection, pc: RTCPeerConnection,
remoteDescription: RTCSessionDescriptionInit, remoteDescription: RTCSessionDescriptionInit,
) { ) {
setLoadingMessage("Setting remote description"); setLoadingMessage("Setting remote description type:" + remoteDescription.type);
try { try {
await pc.setRemoteDescription(new RTCSessionDescription(remoteDescription)); await pc.setRemoteDescription(new RTCSessionDescription(remoteDescription));
console.log("[setRemoteSessionDescription] Remote description set successfully"); console.log("[setRemoteSessionDescription] Remote description set successfully to: " + remoteDescription.sdp);
setLoadingMessage("Establishing secure connection..."); setLoadingMessage("Establishing secure connection...");
} catch (error) { } catch (error) {
console.error( console.error(
@ -231,9 +218,15 @@ export default function KvmIdRoute() {
const ignoreOffer = useRef(false); const ignoreOffer = useRef(false);
const isSettingRemoteAnswerPending = useRef(false); const isSettingRemoteAnswerPending = useRef(false);
const makingOffer = useRef(false); const makingOffer = useRef(false);
const reconnectAttemptsRef = useRef(2000);
const wsProtocol = window.location.protocol === "https:" ? "wss:" : "ws:"; const wsProtocol = window.location.protocol === "https:" ? "wss:" : "ws:";
const reconnectInterval = (attempt: number) => {
// Exponential backoff with a max of 10 seconds between attempts
return Math.min(500 * 2 ** attempt, 10000);
}
const { sendMessage, getWebSocket } = useWebSocket( const { sendMessage, getWebSocket } = useWebSocket(
isOnDevice isOnDevice
? `${wsProtocol}//${window.location.host}/webrtc/signaling/client` ? `${wsProtocol}//${window.location.host}/webrtc/signaling/client`
@ -241,17 +234,16 @@ export default function KvmIdRoute() {
{ {
heartbeat: true, heartbeat: true,
retryOnError: true, retryOnError: true,
reconnectAttempts: 15, reconnectAttempts: reconnectAttemptsRef.current,
reconnectInterval: 1000, reconnectInterval: reconnectInterval,
onReconnectStop: () => { onReconnectStop: (attempt: number) => {
console.debug("Reconnect stopped"); console.debug("Reconnect stopped after ", attempt, "attempts");
cleanupAndStopReconnecting(); cleanupAndStopReconnecting();
}, },
shouldReconnect(event) { shouldReconnect(event) {
console.debug("[Websocket] shouldReconnect", event); console.debug("[Websocket] shouldReconnect", event);
// TODO: Why true? return !connectionFailed; // we always want to try to reconnect unless we're explicitly stopped
return true;
}, },
onClose(event) { onClose(event) {
@ -284,6 +276,7 @@ export default function KvmIdRoute() {
*/ */
const parsedMessage = JSON.parse(message.data); const parsedMessage = JSON.parse(message.data);
if (parsedMessage.type === "device-metadata") { if (parsedMessage.type === "device-metadata") {
const { deviceVersion } = parsedMessage.data; const { deviceVersion } = parsedMessage.data;
console.debug("[Websocket] Received device-metadata message"); console.debug("[Websocket] Received device-metadata message");
@ -300,10 +293,12 @@ export default function KvmIdRoute() {
console.log("[Websocket] Device is using new signaling"); console.log("[Websocket] Device is using new signaling");
isLegacySignalingEnabled.current = false; isLegacySignalingEnabled.current = false;
} }
setupPeerConnection(); setupPeerConnection();
} }
if (!peerConnection) return; if (!peerConnection) return;
if (parsedMessage.type === "answer") { if (parsedMessage.type === "answer") {
console.debug("[Websocket] Received answer"); console.debug("[Websocket] Received answer");
const readyForOffer = const readyForOffer =
@ -594,7 +589,9 @@ export default function KvmIdRoute() {
api.POST(`${CLOUD_API}/webrtc/turn_activity`, { api.POST(`${CLOUD_API}/webrtc/turn_activity`, {
bytesReceived: bytesReceivedDelta, bytesReceived: bytesReceivedDelta,
bytesSent: bytesSentDelta, bytesSent: bytesSentDelta,
}); }).catch(() => {
// we don't care about errors here, but we don't want unhandled promise rejections
});
}, 10000); }, 10000);
const { setNetworkState} = useNetworkStateStore(); const { setNetworkState} = useNetworkStateStore();