mirror of https://github.com/jetkvm/kvm.git
Release 0.3.6
This commit is contained in:
commit
99b3017344
4
Makefile
4
Makefile
|
@ -1,5 +1,5 @@
|
||||||
VERSION_DEV := 0.3.6-dev$(shell date +%Y%m%d%H%M)
|
VERSION_DEV := 0.3.7-dev$(shell date +%Y%m%d%H%M)
|
||||||
VERSION := 0.3.5
|
VERSION := 0.3.6
|
||||||
|
|
||||||
hash_resource:
|
hash_resource:
|
||||||
@shasum -a 256 resource/jetkvm_native | cut -d ' ' -f 1 > resource/jetkvm_native.sha256
|
@shasum -a 256 resource/jetkvm_native | cut -d ' ' -f 1 > resource/jetkvm_native.sha256
|
||||||
|
|
48
config.go
48
config.go
|
@ -4,6 +4,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
type WakeOnLanDevice struct {
|
type WakeOnLanDevice struct {
|
||||||
|
@ -12,44 +13,56 @@ type WakeOnLanDevice struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
CloudURL string `json:"cloud_url"`
|
CloudURL string `json:"cloud_url"`
|
||||||
CloudToken string `json:"cloud_token"`
|
CloudToken string `json:"cloud_token"`
|
||||||
GoogleIdentity string `json:"google_identity"`
|
GoogleIdentity string `json:"google_identity"`
|
||||||
JigglerEnabled bool `json:"jiggler_enabled"`
|
JigglerEnabled bool `json:"jiggler_enabled"`
|
||||||
AutoUpdateEnabled bool `json:"auto_update_enabled"`
|
AutoUpdateEnabled bool `json:"auto_update_enabled"`
|
||||||
IncludePreRelease bool `json:"include_pre_release"`
|
IncludePreRelease bool `json:"include_pre_release"`
|
||||||
HashedPassword string `json:"hashed_password"`
|
HashedPassword string `json:"hashed_password"`
|
||||||
LocalAuthToken string `json:"local_auth_token"`
|
LocalAuthToken string `json:"local_auth_token"`
|
||||||
LocalAuthMode string `json:"localAuthMode"` //TODO: fix it with migration
|
LocalAuthMode string `json:"localAuthMode"` //TODO: fix it with migration
|
||||||
WakeOnLanDevices []WakeOnLanDevice `json:"wake_on_lan_devices"`
|
WakeOnLanDevices []WakeOnLanDevice `json:"wake_on_lan_devices"`
|
||||||
|
EdidString string `json:"hdmi_edid_string"`
|
||||||
|
ActiveExtension string `json:"active_extension"`
|
||||||
|
DisplayMaxBrightness int `json:"display_max_brightness"`
|
||||||
|
DisplayDimAfterSec int `json:"display_dim_after_sec"`
|
||||||
|
DisplayOffAfterSec int `json:"display_off_after_sec"`
|
||||||
}
|
}
|
||||||
|
|
||||||
const configPath = "/userdata/kvm_config.json"
|
const configPath = "/userdata/kvm_config.json"
|
||||||
|
|
||||||
var defaultConfig = &Config{
|
var defaultConfig = &Config{
|
||||||
CloudURL: "https://api.jetkvm.com",
|
CloudURL: "https://api.jetkvm.com",
|
||||||
AutoUpdateEnabled: true, // Set a default value
|
AutoUpdateEnabled: true, // Set a default value
|
||||||
|
ActiveExtension: "",
|
||||||
|
DisplayMaxBrightness: 64,
|
||||||
|
DisplayDimAfterSec: 120, // 2 minutes
|
||||||
|
DisplayOffAfterSec: 1800, // 30 minutes
|
||||||
}
|
}
|
||||||
|
|
||||||
var config *Config
|
var (
|
||||||
|
config *Config
|
||||||
|
configLock = &sync.Mutex{}
|
||||||
|
)
|
||||||
|
|
||||||
func LoadConfig() {
|
func LoadConfig() {
|
||||||
if config != nil {
|
if config != nil {
|
||||||
|
logger.Info("config already loaded, skipping")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
file, err := os.Open(configPath)
|
file, err := os.Open(configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Debug("default config file doesn't exist, using default")
|
logger.Debug("default config file doesn't exist, using default")
|
||||||
config = defaultConfig
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
var loadedConfig Config
|
// load and merge the default config with the user config
|
||||||
|
loadedConfig := *defaultConfig
|
||||||
if err := json.NewDecoder(file).Decode(&loadedConfig); err != nil {
|
if err := json.NewDecoder(file).Decode(&loadedConfig); err != nil {
|
||||||
logger.Errorf("config file JSON parsing failed, %v", err)
|
logger.Errorf("config file JSON parsing failed, %v", err)
|
||||||
config = defaultConfig
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,6 +70,9 @@ func LoadConfig() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func SaveConfig() error {
|
func SaveConfig() error {
|
||||||
|
configLock.Lock()
|
||||||
|
defer configLock.Unlock()
|
||||||
|
|
||||||
file, err := os.Create(configPath)
|
file, err := os.Create(configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create config file: %w", err)
|
return fmt.Errorf("failed to create config file: %w", err)
|
||||||
|
|
168
display.go
168
display.go
|
@ -1,12 +1,26 @@
|
||||||
package kvm
|
package kvm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
var currentScreen = "ui_Boot_Screen"
|
var currentScreen = "ui_Boot_Screen"
|
||||||
|
var backlightState = 0 // 0 - NORMAL, 1 - DIMMED, 2 - OFF
|
||||||
|
|
||||||
|
var (
|
||||||
|
dimTicker *time.Ticker
|
||||||
|
offTicker *time.Ticker
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
touchscreenDevice string = "/dev/input/event1"
|
||||||
|
backlightControlClass string = "/sys/class/backlight/backlight/brightness"
|
||||||
|
)
|
||||||
|
|
||||||
func switchToScreen(screen string) {
|
func switchToScreen(screen string) {
|
||||||
_, err := CallCtrlAction("lv_scr_load", map[string]interface{}{"obj": screen})
|
_, err := CallCtrlAction("lv_scr_load", map[string]interface{}{"obj": screen})
|
||||||
|
@ -65,6 +79,7 @@ func requestDisplayUpdate() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
go func() {
|
go func() {
|
||||||
|
wakeDisplay(false)
|
||||||
fmt.Println("display updating........................")
|
fmt.Println("display updating........................")
|
||||||
//TODO: only run once regardless how many pending updates
|
//TODO: only run once regardless how many pending updates
|
||||||
updateDisplay()
|
updateDisplay()
|
||||||
|
@ -83,6 +98,155 @@ func updateStaticContents() {
|
||||||
updateLabelIfChanged("ui_Status_Content_Device_Id_Content_Label", GetDeviceID())
|
updateLabelIfChanged("ui_Status_Content_Device_Id_Content_Label", GetDeviceID())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// setDisplayBrightness sets /sys/class/backlight/backlight/brightness to alter
|
||||||
|
// the backlight brightness of the JetKVM hardware's display.
|
||||||
|
func setDisplayBrightness(brightness int) error {
|
||||||
|
// NOTE: The actual maximum value for this is 255, but out-of-the-box, the value is set to 64.
|
||||||
|
// The maximum set here is set to 100 to reduce the risk of drawing too much power (and besides, 255 is very bright!).
|
||||||
|
if brightness > 100 || brightness < 0 {
|
||||||
|
return errors.New("brightness value out of bounds, must be between 0 and 100")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the display backlight class is available
|
||||||
|
if _, err := os.Stat(backlightControlClass); errors.Is(err, os.ErrNotExist) {
|
||||||
|
return errors.New("brightness value cannot be set, possibly not running on JetKVM hardware")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the value
|
||||||
|
bs := []byte(strconv.Itoa(brightness))
|
||||||
|
err := os.WriteFile(backlightControlClass, bs, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("display: set brightness to %v\n", brightness)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// tick_displayDim() is called when when dim ticker expires, it simply reduces the brightness
|
||||||
|
// of the display by half of the max brightness.
|
||||||
|
func tick_displayDim() {
|
||||||
|
err := setDisplayBrightness(config.DisplayMaxBrightness / 2)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("display: failed to dim display: %s\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dimTicker.Stop()
|
||||||
|
|
||||||
|
backlightState = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// tick_displayOff() is called when the off ticker expires, it turns off the display
|
||||||
|
// by setting the brightness to zero.
|
||||||
|
func tick_displayOff() {
|
||||||
|
err := setDisplayBrightness(0)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("display: failed to turn off display: %s\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
offTicker.Stop()
|
||||||
|
|
||||||
|
backlightState = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// wakeDisplay sets the display brightness back to config.DisplayMaxBrightness and stores the time the display
|
||||||
|
// last woke, ready for displayTimeoutTick to put the display back in the dim/off states.
|
||||||
|
// Set force to true to skip the backlight state check, this should be done if altering the tickers.
|
||||||
|
func wakeDisplay(force bool) {
|
||||||
|
if backlightState == 0 && !force {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't try to wake up if the display is turned off.
|
||||||
|
if config.DisplayMaxBrightness == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := setDisplayBrightness(config.DisplayMaxBrightness)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("display wake failed, %s\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.DisplayDimAfterSec != 0 {
|
||||||
|
dimTicker.Reset(time.Duration(config.DisplayDimAfterSec) * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.DisplayOffAfterSec != 0 {
|
||||||
|
offTicker.Reset(time.Duration(config.DisplayOffAfterSec) * time.Second)
|
||||||
|
}
|
||||||
|
backlightState = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// watchTsEvents monitors the touchscreen for events and simply calls wakeDisplay() to ensure the
|
||||||
|
// touchscreen interface still works even with LCD dimming/off.
|
||||||
|
// TODO: This is quite a hack, really we should be getting an event from jetkvm_native, or the whole display backlight
|
||||||
|
// control should be hoisted up to jetkvm_native.
|
||||||
|
func watchTsEvents() {
|
||||||
|
ts, err := os.OpenFile(touchscreenDevice, os.O_RDONLY, 0666)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("display: failed to open touchscreen device: %s\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
|
// This buffer is set to 24 bytes as that's the normal size of events on /dev/input
|
||||||
|
// Reference: https://www.kernel.org/doc/Documentation/input/input.txt
|
||||||
|
// This could potentially be set higher, to require multiple events to wake the display.
|
||||||
|
buf := make([]byte, 24)
|
||||||
|
for {
|
||||||
|
_, err := ts.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("display: failed to read from touchscreen device: %s\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
wakeDisplay(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// startBacklightTickers starts the two tickers for dimming and switching off the display
|
||||||
|
// if they're not already set. This is done separately to the init routine as the "never dim"
|
||||||
|
// option has the value set to zero, but time.NewTicker only accept positive values.
|
||||||
|
func startBacklightTickers() {
|
||||||
|
// Don't start the tickers if the display is switched off.
|
||||||
|
// Set the display to off if that's the case.
|
||||||
|
if config.DisplayMaxBrightness == 0 {
|
||||||
|
setDisplayBrightness(0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if dimTicker == nil && config.DisplayDimAfterSec != 0 {
|
||||||
|
fmt.Printf("display: dim_ticker has started\n")
|
||||||
|
dimTicker = time.NewTicker(time.Duration(config.DisplayDimAfterSec) * time.Second)
|
||||||
|
defer dimTicker.Stop()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-dimTicker.C:
|
||||||
|
tick_displayDim()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
if offTicker == nil && config.DisplayOffAfterSec != 0 {
|
||||||
|
fmt.Printf("display: off_ticker has started\n")
|
||||||
|
offTicker = time.NewTicker(time.Duration(config.DisplayOffAfterSec) * time.Second)
|
||||||
|
defer offTicker.Stop()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-offTicker.C:
|
||||||
|
tick_displayOff()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
go func() {
|
go func() {
|
||||||
waitCtrlClientConnected()
|
waitCtrlClientConnected()
|
||||||
|
@ -91,6 +255,10 @@ func init() {
|
||||||
updateStaticContents()
|
updateStaticContents()
|
||||||
displayInited = true
|
displayInited = true
|
||||||
fmt.Println("display inited")
|
fmt.Println("display inited")
|
||||||
|
startBacklightTickers()
|
||||||
|
wakeDisplay(true)
|
||||||
requestDisplayUpdate()
|
requestDisplayUpdate()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
go watchTsEvents()
|
||||||
}
|
}
|
||||||
|
|
5
go.mod
5
go.mod
|
@ -14,6 +14,7 @@ require (
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/gwatts/rootcerts v0.0.0-20240401182218-3ab9db955caf
|
github.com/gwatts/rootcerts v0.0.0-20240401182218-3ab9db955caf
|
||||||
github.com/hanwen/go-fuse/v2 v2.5.1
|
github.com/hanwen/go-fuse/v2 v2.5.1
|
||||||
|
github.com/hashicorp/go-envparse v0.1.0
|
||||||
github.com/openstadia/go-usb-gadget v0.0.0-20231115171102-aebd56bbb965
|
github.com/openstadia/go-usb-gadget v0.0.0-20231115171102-aebd56bbb965
|
||||||
github.com/pion/logging v0.2.2
|
github.com/pion/logging v0.2.2
|
||||||
github.com/pion/mdns/v2 v2.0.7
|
github.com/pion/mdns/v2 v2.0.7
|
||||||
|
@ -21,6 +22,7 @@ require (
|
||||||
github.com/pojntfx/go-nbd v0.3.2
|
github.com/pojntfx/go-nbd v0.3.2
|
||||||
github.com/psanford/httpreadat v0.1.0
|
github.com/psanford/httpreadat v0.1.0
|
||||||
github.com/vishvananda/netlink v1.3.0
|
github.com/vishvananda/netlink v1.3.0
|
||||||
|
go.bug.st/serial v1.6.2
|
||||||
golang.org/x/crypto v0.28.0
|
golang.org/x/crypto v0.28.0
|
||||||
golang.org/x/net v0.30.0
|
golang.org/x/net v0.30.0
|
||||||
)
|
)
|
||||||
|
@ -32,6 +34,7 @@ require (
|
||||||
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||||
|
github.com/creack/goselect v0.1.2 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||||
github.com/go-jose/go-jose/v4 v4.0.2 // indirect
|
github.com/go-jose/go-jose/v4 v4.0.2 // indirect
|
||||||
|
@ -68,7 +71,7 @@ require (
|
||||||
github.com/wlynxg/anet v0.0.5 // indirect
|
github.com/wlynxg/anet v0.0.5 // indirect
|
||||||
golang.org/x/arch v0.8.0 // indirect
|
golang.org/x/arch v0.8.0 // indirect
|
||||||
golang.org/x/oauth2 v0.21.0 // indirect
|
golang.org/x/oauth2 v0.21.0 // indirect
|
||||||
golang.org/x/sys v0.26.0 // indirect
|
golang.org/x/sys v0.29.0 // indirect
|
||||||
golang.org/x/text v0.19.0 // indirect
|
golang.org/x/text v0.19.0 // indirect
|
||||||
google.golang.org/protobuf v1.34.0 // indirect
|
google.golang.org/protobuf v1.34.0 // indirect
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||||
|
|
10
go.sum
10
go.sum
|
@ -16,6 +16,8 @@ github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NA
|
||||||
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
||||||
github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI=
|
github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI=
|
||||||
github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0=
|
github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0=
|
||||||
|
github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0=
|
||||||
|
github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY=
|
||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
github.com/creack/pty v1.1.23 h1:4M6+isWdcStXEf15G/RbrMPOQj1dZ7HPZCGwE4kOeP0=
|
github.com/creack/pty v1.1.23 h1:4M6+isWdcStXEf15G/RbrMPOQj1dZ7HPZCGwE4kOeP0=
|
||||||
github.com/creack/pty v1.1.23/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
github.com/creack/pty v1.1.23/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||||
|
@ -49,6 +51,8 @@ github.com/gwatts/rootcerts v0.0.0-20240401182218-3ab9db955caf h1:JO6ISZIvEUitto
|
||||||
github.com/gwatts/rootcerts v0.0.0-20240401182218-3ab9db955caf/go.mod h1:5Kt9XkWvkGi2OHOq0QsGxebHmhCcqJ8KCbNg/a6+n+g=
|
github.com/gwatts/rootcerts v0.0.0-20240401182218-3ab9db955caf/go.mod h1:5Kt9XkWvkGi2OHOq0QsGxebHmhCcqJ8KCbNg/a6+n+g=
|
||||||
github.com/hanwen/go-fuse/v2 v2.5.1 h1:OQBE8zVemSocRxA4OaFJbjJ5hlpCmIWbGr7r0M4uoQQ=
|
github.com/hanwen/go-fuse/v2 v2.5.1 h1:OQBE8zVemSocRxA4OaFJbjJ5hlpCmIWbGr7r0M4uoQQ=
|
||||||
github.com/hanwen/go-fuse/v2 v2.5.1/go.mod h1:xKwi1cF7nXAOBCXujD5ie0ZKsxc8GGSA1rlMJc+8IJs=
|
github.com/hanwen/go-fuse/v2 v2.5.1/go.mod h1:xKwi1cF7nXAOBCXujD5ie0ZKsxc8GGSA1rlMJc+8IJs=
|
||||||
|
github.com/hashicorp/go-envparse v0.1.0 h1:bE++6bhIsNCPLvgDZkYqo3nA+/PFI51pkrHdmPSDFPY=
|
||||||
|
github.com/hashicorp/go-envparse v0.1.0/go.mod h1:OHheN1GoygLlAkTlXLXvAdnXdZxy8JUweQ1rAXx1xnc=
|
||||||
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/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
|
@ -144,6 +148,8 @@ github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1Y
|
||||||
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
||||||
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
|
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
|
||||||
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
||||||
|
go.bug.st/serial v1.6.2 h1:kn9LRX3sdm+WxWKufMlIRndwGfPWsH1/9lCWXQCasq8=
|
||||||
|
go.bug.st/serial v1.6.2/go.mod h1:UABfsluHAiaNI+La2iESysd9Vetq7VRdpxvjx7CmmOE=
|
||||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||||
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
||||||
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||||
|
@ -159,8 +165,8 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.5.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.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
|
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
|
||||||
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||||
google.golang.org/protobuf v1.34.0 h1:Qo/qEd2RZPCf2nKuorzksSknv0d3ERwp1vFG38gSmH4=
|
google.golang.org/protobuf v1.34.0 h1:Qo/qEd2RZPCf2nKuorzksSknv0d3ERwp1vFG38gSmH4=
|
||||||
|
|
242
jsonrpc.go
242
jsonrpc.go
|
@ -10,8 +10,11 @@ import (
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/pion/webrtc/v4"
|
"github.com/pion/webrtc/v4"
|
||||||
|
"go.bug.st/serial"
|
||||||
)
|
)
|
||||||
|
|
||||||
type JSONRPCRequest struct {
|
type JSONRPCRequest struct {
|
||||||
|
@ -34,6 +37,12 @@ type JSONRPCEvent struct {
|
||||||
Params interface{} `json:"params,omitempty"`
|
Params interface{} `json:"params,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type BacklightSettings struct {
|
||||||
|
MaxBrightness int `json:"max_brightness"`
|
||||||
|
DimAfter int `json:"dim_after"`
|
||||||
|
OffAfter int `json:"off_after"`
|
||||||
|
}
|
||||||
|
|
||||||
func writeJSONRPCResponse(response JSONRPCResponse, session *Session) {
|
func writeJSONRPCResponse(response JSONRPCResponse, session *Session) {
|
||||||
responseBytes, err := json.Marshal(response)
|
responseBytes, err := json.Marshal(response)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -183,6 +192,11 @@ func rpcSetEDID(edid string) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save EDID to config, allowing it to be restored on reboot.
|
||||||
|
config.EdidString = edid
|
||||||
|
SaveConfig()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -219,6 +233,52 @@ func rpcTryUpdate() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func rpcSetBacklightSettings(params BacklightSettings) error {
|
||||||
|
blConfig := params
|
||||||
|
|
||||||
|
// NOTE: by default, the frontend limits the brightness to 64, as that's what the device originally shipped with.
|
||||||
|
if blConfig.MaxBrightness > 255 || blConfig.MaxBrightness < 0 {
|
||||||
|
return fmt.Errorf("maxBrightness must be between 0 and 255")
|
||||||
|
}
|
||||||
|
|
||||||
|
if blConfig.DimAfter < 0 {
|
||||||
|
return fmt.Errorf("dimAfter must be a positive integer")
|
||||||
|
}
|
||||||
|
|
||||||
|
if blConfig.OffAfter < 0 {
|
||||||
|
return fmt.Errorf("offAfter must be a positive integer")
|
||||||
|
}
|
||||||
|
|
||||||
|
config.DisplayMaxBrightness = blConfig.MaxBrightness
|
||||||
|
config.DisplayDimAfterSec = blConfig.DimAfter
|
||||||
|
config.DisplayOffAfterSec = blConfig.OffAfter
|
||||||
|
|
||||||
|
if err := SaveConfig(); err != nil {
|
||||||
|
return fmt.Errorf("failed to save config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("rpc: display: settings applied, max_brightness: %d, dim after: %ds, off after: %ds", config.DisplayMaxBrightness, config.DisplayDimAfterSec, config.DisplayOffAfterSec)
|
||||||
|
|
||||||
|
// If the device started up with auto-dim and/or auto-off set to zero, the display init
|
||||||
|
// method will not have started the tickers. So in case that has changed, attempt to start the tickers now.
|
||||||
|
startBacklightTickers()
|
||||||
|
|
||||||
|
// Wake the display after the settings are altered, this ensures the tickers
|
||||||
|
// are reset to the new settings, and will bring the display up to maxBrightness.
|
||||||
|
// Calling with force set to true, to ignore the current state of the display, and force
|
||||||
|
// it to reset the tickers.
|
||||||
|
wakeDisplay(true)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func rpcGetBacklightSettings() (*BacklightSettings, error) {
|
||||||
|
return &BacklightSettings{
|
||||||
|
MaxBrightness: config.DisplayMaxBrightness,
|
||||||
|
DimAfter: int(config.DisplayDimAfterSec),
|
||||||
|
OffAfter: int(config.DisplayOffAfterSec),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
devModeFile = "/userdata/jetkvm/devmode.enable"
|
devModeFile = "/userdata/jetkvm/devmode.enable"
|
||||||
sshKeyDir = "/userdata/dropbear/.ssh"
|
sshKeyDir = "/userdata/dropbear/.ssh"
|
||||||
|
@ -379,7 +439,7 @@ func callRPCHandler(handler RPCHandler, params map[string]interface{}) (interfac
|
||||||
}
|
}
|
||||||
args[i] = reflect.ValueOf(newStruct).Elem()
|
args[i] = reflect.ValueOf(newStruct).Elem()
|
||||||
} else {
|
} else {
|
||||||
return nil, fmt.Errorf("invalid parameter type for: %s", paramName)
|
return nil, fmt.Errorf("invalid parameter type for: %s, type: %s", paramName, paramType.Kind())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
args[i] = convertedValue.Convert(paramType)
|
args[i] = convertedValue.Convert(paramType)
|
||||||
|
@ -479,7 +539,6 @@ func rpcSetUsbEmulationState(enabled bool) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func rpcGetWakeOnLanDevices() ([]WakeOnLanDevice, error) {
|
func rpcGetWakeOnLanDevices() ([]WakeOnLanDevice, error) {
|
||||||
LoadConfig()
|
|
||||||
if config.WakeOnLanDevices == nil {
|
if config.WakeOnLanDevices == nil {
|
||||||
return []WakeOnLanDevice{}, nil
|
return []WakeOnLanDevice{}, nil
|
||||||
}
|
}
|
||||||
|
@ -491,13 +550,11 @@ type SetWakeOnLanDevicesParams struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func rpcSetWakeOnLanDevices(params SetWakeOnLanDevicesParams) error {
|
func rpcSetWakeOnLanDevices(params SetWakeOnLanDevicesParams) error {
|
||||||
LoadConfig()
|
|
||||||
config.WakeOnLanDevices = params.Devices
|
config.WakeOnLanDevices = params.Devices
|
||||||
return SaveConfig()
|
return SaveConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
func rpcResetConfig() error {
|
func rpcResetConfig() error {
|
||||||
LoadConfig()
|
|
||||||
config = defaultConfig
|
config = defaultConfig
|
||||||
if err := SaveConfig(); err != nil {
|
if err := SaveConfig(); err != nil {
|
||||||
return fmt.Errorf("failed to reset config: %w", err)
|
return fmt.Errorf("failed to reset config: %w", err)
|
||||||
|
@ -507,7 +564,172 @@ func rpcResetConfig() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: replace this crap with code generator
|
type DCPowerState struct {
|
||||||
|
IsOn bool `json:"isOn"`
|
||||||
|
Voltage float64 `json:"voltage"`
|
||||||
|
Current float64 `json:"current"`
|
||||||
|
Power float64 `json:"power"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func rpcGetDCPowerState() (DCPowerState, error) {
|
||||||
|
return dcState, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func rpcSetDCPowerState(enabled bool) error {
|
||||||
|
log.Printf("[jsonrpc.go:rpcSetDCPowerState] Setting DC power state to: %v", enabled)
|
||||||
|
err := setDCPowerState(enabled)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to set DC power state: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func rpcGetActiveExtension() (string, error) {
|
||||||
|
return config.ActiveExtension, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func rpcSetActiveExtension(extensionId string) error {
|
||||||
|
if config.ActiveExtension == extensionId {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if config.ActiveExtension == "atx-power" {
|
||||||
|
unmountATXControl()
|
||||||
|
} else if config.ActiveExtension == "dc-power" {
|
||||||
|
unmountDCControl()
|
||||||
|
}
|
||||||
|
config.ActiveExtension = extensionId
|
||||||
|
if err := SaveConfig(); err != nil {
|
||||||
|
return fmt.Errorf("failed to save config: %w", err)
|
||||||
|
}
|
||||||
|
if extensionId == "atx-power" {
|
||||||
|
mountATXControl()
|
||||||
|
} else if extensionId == "dc-power" {
|
||||||
|
mountDCControl()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func rpcSetATXPowerAction(action string) error {
|
||||||
|
logger.Debugf("[jsonrpc.go:rpcSetATXPowerAction] Executing ATX power action: %s", action)
|
||||||
|
switch action {
|
||||||
|
case "power-short":
|
||||||
|
logger.Debug("[jsonrpc.go:rpcSetATXPowerAction] Simulating short power button press")
|
||||||
|
return pressATXPowerButton(200 * time.Millisecond)
|
||||||
|
case "power-long":
|
||||||
|
logger.Debug("[jsonrpc.go:rpcSetATXPowerAction] Simulating long power button press")
|
||||||
|
return pressATXPowerButton(5 * time.Second)
|
||||||
|
case "reset":
|
||||||
|
logger.Debug("[jsonrpc.go:rpcSetATXPowerAction] Simulating reset button press")
|
||||||
|
return pressATXResetButton(200 * time.Millisecond)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid action: %s", action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ATXState struct {
|
||||||
|
Power bool `json:"power"`
|
||||||
|
HDD bool `json:"hdd"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func rpcGetATXState() (ATXState, error) {
|
||||||
|
state := ATXState{
|
||||||
|
Power: ledPWRState,
|
||||||
|
HDD: ledHDDState,
|
||||||
|
}
|
||||||
|
return state, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type SerialSettings struct {
|
||||||
|
BaudRate string `json:"baudRate"`
|
||||||
|
DataBits string `json:"dataBits"`
|
||||||
|
StopBits string `json:"stopBits"`
|
||||||
|
Parity string `json:"parity"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func rpcGetSerialSettings() (SerialSettings, error) {
|
||||||
|
settings := SerialSettings{
|
||||||
|
BaudRate: strconv.Itoa(serialPortMode.BaudRate),
|
||||||
|
DataBits: strconv.Itoa(serialPortMode.DataBits),
|
||||||
|
StopBits: "1",
|
||||||
|
Parity: "none",
|
||||||
|
}
|
||||||
|
|
||||||
|
switch serialPortMode.StopBits {
|
||||||
|
case serial.OneStopBit:
|
||||||
|
settings.StopBits = "1"
|
||||||
|
case serial.OnePointFiveStopBits:
|
||||||
|
settings.StopBits = "1.5"
|
||||||
|
case serial.TwoStopBits:
|
||||||
|
settings.StopBits = "2"
|
||||||
|
}
|
||||||
|
|
||||||
|
switch serialPortMode.Parity {
|
||||||
|
case serial.NoParity:
|
||||||
|
settings.Parity = "none"
|
||||||
|
case serial.OddParity:
|
||||||
|
settings.Parity = "odd"
|
||||||
|
case serial.EvenParity:
|
||||||
|
settings.Parity = "even"
|
||||||
|
case serial.MarkParity:
|
||||||
|
settings.Parity = "mark"
|
||||||
|
case serial.SpaceParity:
|
||||||
|
settings.Parity = "space"
|
||||||
|
}
|
||||||
|
|
||||||
|
return settings, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var serialPortMode = defaultMode
|
||||||
|
|
||||||
|
func rpcSetSerialSettings(settings SerialSettings) error {
|
||||||
|
baudRate, err := strconv.Atoi(settings.BaudRate)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid baud rate: %v", err)
|
||||||
|
}
|
||||||
|
dataBits, err := strconv.Atoi(settings.DataBits)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid data bits: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var stopBits serial.StopBits
|
||||||
|
switch settings.StopBits {
|
||||||
|
case "1":
|
||||||
|
stopBits = serial.OneStopBit
|
||||||
|
case "1.5":
|
||||||
|
stopBits = serial.OnePointFiveStopBits
|
||||||
|
case "2":
|
||||||
|
stopBits = serial.TwoStopBits
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid stop bits: %s", settings.StopBits)
|
||||||
|
}
|
||||||
|
|
||||||
|
var parity serial.Parity
|
||||||
|
switch settings.Parity {
|
||||||
|
case "none":
|
||||||
|
parity = serial.NoParity
|
||||||
|
case "odd":
|
||||||
|
parity = serial.OddParity
|
||||||
|
case "even":
|
||||||
|
parity = serial.EvenParity
|
||||||
|
case "mark":
|
||||||
|
parity = serial.MarkParity
|
||||||
|
case "space":
|
||||||
|
parity = serial.SpaceParity
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid parity: %s", settings.Parity)
|
||||||
|
}
|
||||||
|
serialPortMode = &serial.Mode{
|
||||||
|
BaudRate: baudRate,
|
||||||
|
DataBits: dataBits,
|
||||||
|
StopBits: stopBits,
|
||||||
|
Parity: parity,
|
||||||
|
}
|
||||||
|
|
||||||
|
port.SetMode(serialPortMode)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
var rpcHandlers = map[string]RPCHandler{
|
var rpcHandlers = map[string]RPCHandler{
|
||||||
"ping": {Func: rpcPing},
|
"ping": {Func: rpcPing},
|
||||||
"getDeviceID": {Func: rpcGetDeviceID},
|
"getDeviceID": {Func: rpcGetDeviceID},
|
||||||
|
@ -554,4 +776,14 @@ var rpcHandlers = map[string]RPCHandler{
|
||||||
"getWakeOnLanDevices": {Func: rpcGetWakeOnLanDevices},
|
"getWakeOnLanDevices": {Func: rpcGetWakeOnLanDevices},
|
||||||
"setWakeOnLanDevices": {Func: rpcSetWakeOnLanDevices, Params: []string{"params"}},
|
"setWakeOnLanDevices": {Func: rpcSetWakeOnLanDevices, Params: []string{"params"}},
|
||||||
"resetConfig": {Func: rpcResetConfig},
|
"resetConfig": {Func: rpcResetConfig},
|
||||||
|
"setBacklightSettings": {Func: rpcSetBacklightSettings, Params: []string{"params"}},
|
||||||
|
"getBacklightSettings": {Func: rpcGetBacklightSettings},
|
||||||
|
"getDCPowerState": {Func: rpcGetDCPowerState},
|
||||||
|
"setDCPowerState": {Func: rpcSetDCPowerState, Params: []string{"enabled"}},
|
||||||
|
"getActiveExtension": {Func: rpcGetActiveExtension},
|
||||||
|
"setActiveExtension": {Func: rpcSetActiveExtension, Params: []string{"extensionId"}},
|
||||||
|
"getATXState": {Func: rpcGetATXState},
|
||||||
|
"setATXPowerAction": {Func: rpcSetATXPowerAction, Params: []string{"action"}},
|
||||||
|
"getSerialSettings": {Func: rpcGetSerialSettings},
|
||||||
|
"setSerialSettings": {Func: rpcSetSerialSettings, Params: []string{"settings"}},
|
||||||
}
|
}
|
||||||
|
|
1
main.go
1
main.go
|
@ -71,6 +71,7 @@ func Main() {
|
||||||
if config.CloudToken != "" {
|
if config.CloudToken != "" {
|
||||||
go RunWebsocketClient()
|
go RunWebsocketClient()
|
||||||
}
|
}
|
||||||
|
initSerialPort()
|
||||||
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
|
||||||
|
|
15
native.go
15
native.go
|
@ -152,6 +152,9 @@ func handleCtrlClient(conn net.Conn) {
|
||||||
|
|
||||||
ctrlSocketConn = conn
|
ctrlSocketConn = conn
|
||||||
|
|
||||||
|
// Restore HDMI EDID if applicable
|
||||||
|
go restoreHdmiEdid()
|
||||||
|
|
||||||
readBuf := make([]byte, 4096)
|
readBuf := make([]byte, 4096)
|
||||||
for {
|
for {
|
||||||
n, err := conn.Read(readBuf)
|
n, err := conn.Read(readBuf)
|
||||||
|
@ -304,3 +307,15 @@ func ensureBinaryUpdated(destPath string) error {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Restore the HDMI EDID value from the config.
|
||||||
|
// Called after successful connection to jetkvm_native.
|
||||||
|
func restoreHdmiEdid() {
|
||||||
|
if config.EdidString != "" {
|
||||||
|
logger.Infof("Restoring HDMI EDID to %v", config.EdidString)
|
||||||
|
_, err := CallCtrlAction("set_edid", map[string]interface{}{"edid": config.EdidString})
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("Failed to restore HDMI EDID: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
129
network.go
129
network.go
|
@ -1,22 +1,35 @@
|
||||||
package kvm
|
package kvm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"os/exec"
|
||||||
|
|
||||||
|
"github.com/hashicorp/go-envparse"
|
||||||
"github.com/pion/mdns/v2"
|
"github.com/pion/mdns/v2"
|
||||||
"golang.org/x/net/ipv4"
|
"golang.org/x/net/ipv4"
|
||||||
"golang.org/x/net/ipv6"
|
"golang.org/x/net/ipv6"
|
||||||
"net"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/vishvananda/netlink"
|
"github.com/vishvananda/netlink"
|
||||||
"github.com/vishvananda/netlink/nl"
|
"github.com/vishvananda/netlink/nl"
|
||||||
)
|
)
|
||||||
|
|
||||||
var networkState struct {
|
var mDNSConn *mdns.Conn
|
||||||
|
|
||||||
|
var networkState NetworkState
|
||||||
|
|
||||||
|
type NetworkState struct {
|
||||||
Up bool
|
Up bool
|
||||||
IPv4 string
|
IPv4 string
|
||||||
IPv6 string
|
IPv6 string
|
||||||
MAC string
|
MAC string
|
||||||
|
|
||||||
|
checked bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type LocalIpInfo struct {
|
type LocalIpInfo struct {
|
||||||
|
@ -25,44 +38,93 @@ type LocalIpInfo struct {
|
||||||
MAC string
|
MAC string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
NetIfName = "eth0"
|
||||||
|
DHCPLeaseFile = "/run/udhcpc.%s.info"
|
||||||
|
)
|
||||||
|
|
||||||
|
// setDhcpClientState sends signals to udhcpc to change it's current mode
|
||||||
|
// of operation. Setting active to true will force udhcpc to renew the DHCP lease.
|
||||||
|
// Setting active to false will put udhcpc into idle mode.
|
||||||
|
func setDhcpClientState(active bool) {
|
||||||
|
var signal string
|
||||||
|
if active {
|
||||||
|
signal = "-SIGUSR1"
|
||||||
|
} else {
|
||||||
|
signal = "-SIGUSR2"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command("/usr/bin/killall", signal, "udhcpc")
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
fmt.Printf("network: setDhcpClientState: failed to change udhcpc state: %s\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func checkNetworkState() {
|
func checkNetworkState() {
|
||||||
iface, err := netlink.LinkByName("eth0")
|
iface, err := netlink.LinkByName(NetIfName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("failed to get eth0 interface: %v\n", err)
|
fmt.Printf("failed to get [%s] interface: %v\n", NetIfName, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
newState := struct {
|
newState := NetworkState{
|
||||||
Up bool
|
|
||||||
IPv4 string
|
|
||||||
IPv6 string
|
|
||||||
MAC string
|
|
||||||
}{
|
|
||||||
Up: iface.Attrs().OperState == netlink.OperUp,
|
Up: iface.Attrs().OperState == netlink.OperUp,
|
||||||
MAC: iface.Attrs().HardwareAddr.String(),
|
MAC: iface.Attrs().HardwareAddr.String(),
|
||||||
|
|
||||||
|
checked: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
addrs, err := netlink.AddrList(iface, nl.FAMILY_ALL)
|
addrs, err := netlink.AddrList(iface, nl.FAMILY_ALL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("failed to get addresses for eth0: %v\n", err)
|
fmt.Printf("failed to get addresses for [%s]: %v\n", NetIfName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the link is going down, put udhcpc into idle mode.
|
||||||
|
// If the link is coming back up, activate udhcpc and force it to renew the lease.
|
||||||
|
if newState.Up != networkState.Up {
|
||||||
|
setDhcpClientState(newState.Up)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, addr := range addrs {
|
for _, addr := range addrs {
|
||||||
if addr.IP.To4() != nil {
|
if addr.IP.To4() != nil {
|
||||||
newState.IPv4 = addr.IP.String()
|
if !newState.Up && networkState.Up {
|
||||||
|
// If the network is going down, remove all IPv4 addresses from the interface.
|
||||||
|
fmt.Printf("network: state transitioned to down, removing IPv4 address %s\n", addr.IP.String())
|
||||||
|
err := netlink.AddrDel(iface, &addr)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("network: failed to delete %s", addr.IP.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
newState.IPv4 = "..."
|
||||||
|
} else {
|
||||||
|
newState.IPv4 = addr.IP.String()
|
||||||
|
}
|
||||||
} else if addr.IP.To16() != nil && newState.IPv6 == "" {
|
} else if addr.IP.To16() != nil && newState.IPv6 == "" {
|
||||||
newState.IPv6 = addr.IP.String()
|
newState.IPv6 = addr.IP.String()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if newState != networkState {
|
if newState != networkState {
|
||||||
networkState = newState
|
|
||||||
fmt.Println("network state changed")
|
fmt.Println("network state changed")
|
||||||
|
// restart MDNS
|
||||||
|
startMDNS()
|
||||||
|
networkState = newState
|
||||||
requestDisplayUpdate()
|
requestDisplayUpdate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func startMDNS() error {
|
func startMDNS() error {
|
||||||
|
// If server was previously running, stop it
|
||||||
|
if mDNSConn != nil {
|
||||||
|
fmt.Printf("Stopping mDNS server\n")
|
||||||
|
err := mDNSConn.Close()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("failed to stop mDNS server: %v\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start a new server
|
||||||
|
fmt.Printf("Starting mDNS server on jetkvm.local\n")
|
||||||
addr4, err := net.ResolveUDPAddr("udp4", mdns.DefaultAddressIPv4)
|
addr4, err := net.ResolveUDPAddr("udp4", mdns.DefaultAddressIPv4)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -83,16 +145,50 @@ func startMDNS() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = mdns.Server(ipv4.NewPacketConn(l4), ipv6.NewPacketConn(l6), &mdns.Config{
|
mDNSConn, err = mdns.Server(ipv4.NewPacketConn(l4), ipv6.NewPacketConn(l6), &mdns.Config{
|
||||||
LocalNames: []string{"jetkvm.local"}, //TODO: make it configurable
|
LocalNames: []string{"jetkvm.local"}, //TODO: make it configurable
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
mDNSConn = nil
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
//defer server.Close()
|
//defer server.Close()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getNTPServersFromDHCPInfo() ([]string, error) {
|
||||||
|
buf, err := os.ReadFile(fmt.Sprintf(DHCPLeaseFile, NetIfName))
|
||||||
|
if err != nil {
|
||||||
|
// do not return error if file does not exist
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("failed to load udhcpc info: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse udhcpc info
|
||||||
|
env, err := envparse.Parse(bytes.NewReader(buf))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse udhcpc info: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
val, ok := env["ntpsrv"]
|
||||||
|
if !ok {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var servers []string
|
||||||
|
|
||||||
|
for _, server := range strings.Fields(val) {
|
||||||
|
if net.ParseIP(server) == nil {
|
||||||
|
fmt.Printf("invalid NTP server IP: %s, ignoring ... \n", server)
|
||||||
|
}
|
||||||
|
servers = append(servers, server)
|
||||||
|
}
|
||||||
|
|
||||||
|
return servers, nil
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
updates := make(chan netlink.LinkUpdate)
|
updates := make(chan netlink.LinkUpdate)
|
||||||
done := make(chan struct{})
|
done := make(chan struct{})
|
||||||
|
@ -111,7 +207,7 @@ func init() {
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case update := <-updates:
|
case update := <-updates:
|
||||||
if update.Link.Attrs().Name == "eth0" {
|
if update.Link.Attrs().Name == NetIfName {
|
||||||
fmt.Printf("link update: %+v\n", update)
|
fmt.Printf("link update: %+v\n", update)
|
||||||
checkNetworkState()
|
checkNetworkState()
|
||||||
}
|
}
|
||||||
|
@ -122,7 +218,6 @@ func init() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
fmt.Println("Starting mDNS server")
|
|
||||||
err := startMDNS()
|
err := startMDNS()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("failed to run mDNS: %v", err)
|
fmt.Println("failed to run mDNS: %v", err)
|
||||||
|
|
61
ntp.go
61
ntp.go
|
@ -11,20 +11,56 @@ import (
|
||||||
"github.com/beevik/ntp"
|
"github.com/beevik/ntp"
|
||||||
)
|
)
|
||||||
|
|
||||||
var timeSynced = false
|
const (
|
||||||
|
timeSyncRetryStep = 5 * time.Second
|
||||||
|
timeSyncRetryMaxInt = 1 * time.Minute
|
||||||
|
timeSyncWaitNetChkInt = 100 * time.Millisecond
|
||||||
|
timeSyncWaitNetUpInt = 3 * time.Second
|
||||||
|
timeSyncInterval = 1 * time.Hour
|
||||||
|
timeSyncTimeout = 2 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
timeSynced = false
|
||||||
|
timeSyncRetryInterval = 0 * time.Second
|
||||||
|
defaultNTPServers = []string{
|
||||||
|
"time.cloudflare.com",
|
||||||
|
"time.apple.com",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
func TimeSyncLoop() {
|
func TimeSyncLoop() {
|
||||||
for {
|
for {
|
||||||
fmt.Println("Syncing system time")
|
if !networkState.checked {
|
||||||
|
time.Sleep(timeSyncWaitNetChkInt)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !networkState.Up {
|
||||||
|
log.Printf("Waiting for network to come up")
|
||||||
|
time.Sleep(timeSyncWaitNetUpInt)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Syncing system time")
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
err := SyncSystemTime()
|
err := SyncSystemTime()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to sync system time: %v", err)
|
log.Printf("Failed to sync system time: %v", err)
|
||||||
|
|
||||||
|
// retry after a delay
|
||||||
|
timeSyncRetryInterval += timeSyncRetryStep
|
||||||
|
time.Sleep(timeSyncRetryInterval)
|
||||||
|
// reset the retry interval if it exceeds the max interval
|
||||||
|
if timeSyncRetryInterval > timeSyncRetryMaxInt {
|
||||||
|
timeSyncRetryInterval = 0
|
||||||
|
}
|
||||||
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
log.Printf("Time sync successful, now is: %v, time taken: %v", time.Now(), time.Since(start))
|
log.Printf("Time sync successful, now is: %v, time taken: %v", time.Now(), time.Since(start))
|
||||||
timeSynced = true
|
timeSynced = true
|
||||||
time.Sleep(1 * time.Hour) //once the first sync is done, sync every hour
|
time.Sleep(timeSyncInterval) // after the first sync is done
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,13 +77,22 @@ func SyncSystemTime() (err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func queryNetworkTime() (*time.Time, error) {
|
func queryNetworkTime() (*time.Time, error) {
|
||||||
ntpServers := []string{
|
ntpServers, err := getNTPServersFromDHCPInfo()
|
||||||
"time.cloudflare.com",
|
if err != nil {
|
||||||
"time.apple.com",
|
log.Printf("failed to get NTP servers from DHCP info: %v\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ntpServers == nil {
|
||||||
|
ntpServers = defaultNTPServers
|
||||||
|
log.Printf("Using default NTP servers: %v\n", ntpServers)
|
||||||
|
} else {
|
||||||
|
log.Printf("Using NTP servers from DHCP: %v\n", ntpServers)
|
||||||
|
}
|
||||||
|
|
||||||
for _, server := range ntpServers {
|
for _, server := range ntpServers {
|
||||||
now, err := queryNtpServer(server, 2*time.Second)
|
now, err := queryNtpServer(server, timeSyncTimeout)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
log.Printf("NTP server [%s] returned time: %v\n", server, now)
|
||||||
return now, nil
|
return now, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -56,7 +101,7 @@ func queryNetworkTime() (*time.Time, error) {
|
||||||
"http://cloudflare.com",
|
"http://cloudflare.com",
|
||||||
}
|
}
|
||||||
for _, url := range httpUrls {
|
for _, url := range httpUrls {
|
||||||
now, err := queryHttpTime(url, 2*time.Second)
|
now, err := queryHttpTime(url, timeSyncTimeout)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return now, nil
|
return now, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,269 @@
|
||||||
|
package kvm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"io"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pion/webrtc/v4"
|
||||||
|
"go.bug.st/serial"
|
||||||
|
)
|
||||||
|
|
||||||
|
const serialPortPath = "/dev/ttyS3"
|
||||||
|
|
||||||
|
var port serial.Port
|
||||||
|
|
||||||
|
func mountATXControl() error {
|
||||||
|
port.SetMode(defaultMode)
|
||||||
|
go runATXControl()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func unmountATXControl() error {
|
||||||
|
reopenSerialPort()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ledHDDState bool
|
||||||
|
ledPWRState bool
|
||||||
|
btnRSTState bool
|
||||||
|
btnPWRState bool
|
||||||
|
)
|
||||||
|
|
||||||
|
func runATXControl() {
|
||||||
|
reader := bufio.NewReader(port)
|
||||||
|
for {
|
||||||
|
line, err := reader.ReadString('\n')
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("Error reading from serial port: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Each line should be 4 binary digits + newline
|
||||||
|
if len(line) != 5 {
|
||||||
|
logger.Warnf("Invalid line length: %d", len(line))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse new states
|
||||||
|
newLedHDDState := line[0] == '0'
|
||||||
|
newLedPWRState := line[1] == '0'
|
||||||
|
newBtnRSTState := line[2] == '1'
|
||||||
|
newBtnPWRState := line[3] == '1'
|
||||||
|
|
||||||
|
if currentSession != nil {
|
||||||
|
writeJSONRPCEvent("atxState", ATXState{
|
||||||
|
Power: newLedPWRState,
|
||||||
|
HDD: newLedHDDState,
|
||||||
|
}, currentSession)
|
||||||
|
}
|
||||||
|
|
||||||
|
if newLedHDDState != ledHDDState ||
|
||||||
|
newLedPWRState != ledPWRState ||
|
||||||
|
newBtnRSTState != btnRSTState ||
|
||||||
|
newBtnPWRState != btnPWRState {
|
||||||
|
|
||||||
|
logger.Debugf("Status changed: HDD LED: %v, PWR LED: %v, RST BTN: %v, PWR BTN: %v",
|
||||||
|
newLedHDDState, newLedPWRState, newBtnRSTState, newBtnPWRState)
|
||||||
|
|
||||||
|
// Update states
|
||||||
|
ledHDDState = newLedHDDState
|
||||||
|
ledPWRState = newLedPWRState
|
||||||
|
btnRSTState = newBtnRSTState
|
||||||
|
btnPWRState = newBtnPWRState
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func pressATXPowerButton(duration time.Duration) error {
|
||||||
|
_, err := port.Write([]byte("\n"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = port.Write([]byte("BTN_PWR_ON\n"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(duration)
|
||||||
|
|
||||||
|
_, err = port.Write([]byte("BTN_PWR_OFF\n"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func pressATXResetButton(duration time.Duration) error {
|
||||||
|
_, err := port.Write([]byte("\n"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = port.Write([]byte("BTN_RST_ON\n"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(duration)
|
||||||
|
|
||||||
|
_, err = port.Write([]byte("BTN_RST_OFF\n"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func mountDCControl() error {
|
||||||
|
port.SetMode(defaultMode)
|
||||||
|
go runDCControl()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func unmountDCControl() error {
|
||||||
|
reopenSerialPort()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var dcState DCPowerState
|
||||||
|
|
||||||
|
func runDCControl() {
|
||||||
|
reader := bufio.NewReader(port)
|
||||||
|
for {
|
||||||
|
line, err := reader.ReadString('\n')
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("Error reading from serial port: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split the line by semicolon
|
||||||
|
parts := strings.Split(strings.TrimSpace(line), ";")
|
||||||
|
if len(parts) != 4 {
|
||||||
|
logger.Warnf("Invalid line: %s", line)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse new states
|
||||||
|
powerState, err := strconv.Atoi(parts[0])
|
||||||
|
if err != nil {
|
||||||
|
logger.Warnf("Invalid power state: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
dcState.IsOn = powerState == 1
|
||||||
|
milliVolts, err := strconv.ParseFloat(parts[1], 64)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warnf("Invalid voltage: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
volts := milliVolts / 1000 // Convert mV to V
|
||||||
|
|
||||||
|
milliAmps, err := strconv.ParseFloat(parts[2], 64)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warnf("Invalid current: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
amps := milliAmps / 1000 // Convert mA to A
|
||||||
|
|
||||||
|
milliWatts, err := strconv.ParseFloat(parts[3], 64)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warnf("Invalid power: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
watts := milliWatts / 1000 // Convert mW to W
|
||||||
|
|
||||||
|
dcState.Voltage = volts
|
||||||
|
dcState.Current = amps
|
||||||
|
dcState.Power = watts
|
||||||
|
|
||||||
|
if currentSession != nil {
|
||||||
|
writeJSONRPCEvent("dcState", dcState, currentSession)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setDCPowerState(on bool) error {
|
||||||
|
_, err := port.Write([]byte("\n"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
command := "PWR_OFF\n"
|
||||||
|
if on {
|
||||||
|
command = "PWR_ON\n"
|
||||||
|
}
|
||||||
|
_, err = port.Write([]byte(command))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultMode = &serial.Mode{
|
||||||
|
BaudRate: 115200,
|
||||||
|
DataBits: 8,
|
||||||
|
Parity: serial.NoParity,
|
||||||
|
StopBits: serial.OneStopBit,
|
||||||
|
}
|
||||||
|
|
||||||
|
func initSerialPort() {
|
||||||
|
reopenSerialPort()
|
||||||
|
if config.ActiveExtension == "atx-power" {
|
||||||
|
mountATXControl()
|
||||||
|
} else if config.ActiveExtension == "dc-power" {
|
||||||
|
mountDCControl()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func reopenSerialPort() error {
|
||||||
|
if port != nil {
|
||||||
|
port.Close()
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
port, err = serial.Open(serialPortPath, defaultMode)
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("Error opening serial port: %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleSerialChannel(d *webrtc.DataChannel) {
|
||||||
|
d.OnOpen(func() {
|
||||||
|
go func() {
|
||||||
|
buf := make([]byte, 1024)
|
||||||
|
for {
|
||||||
|
n, err := port.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
if err != io.EOF {
|
||||||
|
logger.Errorf("Failed to read from serial port: %v", err)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
err = d.Send(buf[:n])
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("Failed to send serial output: %v", err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
})
|
||||||
|
|
||||||
|
d.OnMessage(func(msg webrtc.DataChannelMessage) {
|
||||||
|
if port == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, err := port.Write(msg.Data)
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("Failed to write to serial: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
d.OnClose(func() {
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
|
@ -2,3 +2,5 @@ VITE_SIGNAL_API=http://localhost:3000
|
||||||
|
|
||||||
VITE_CLOUD_APP=http://localhost:5173
|
VITE_CLOUD_APP=http://localhost:5173
|
||||||
VITE_CLOUD_API=http://localhost:3000
|
VITE_CLOUD_API=http://localhost:3000
|
||||||
|
|
||||||
|
VITE_JETKVM_HEAD=
|
|
@ -2,3 +2,5 @@ VITE_SIGNAL_API= # Uses the KVM device's IP address as the signal API endpoint
|
||||||
|
|
||||||
VITE_CLOUD_APP=https://app.jetkvm.com
|
VITE_CLOUD_APP=https://app.jetkvm.com
|
||||||
VITE_CLOUD_API=https://api.jetkvm.com
|
VITE_CLOUD_API=https://api.jetkvm.com
|
||||||
|
|
||||||
|
VITE_JETKVM_HEAD=<script src="/device/ui-config.js"></script>
|
|
@ -1,4 +1,6 @@
|
||||||
VITE_SIGNAL_API=https://api.jetkvm.com
|
VITE_SIGNAL_API=https://api.jetkvm.com
|
||||||
|
|
||||||
VITE_CLOUD_APP=https://app.jetkvm.com
|
VITE_CLOUD_APP=https://app.jetkvm.com
|
||||||
VITE_CLOUD_API=https://api.jetkvm.com
|
VITE_CLOUD_API=https://api.jetkvm.com
|
||||||
|
|
||||||
|
VITE_JETKVM_HEAD=
|
|
@ -0,0 +1,4 @@
|
||||||
|
VITE_SIGNAL_API=https://staging-api.jetkvm.com
|
||||||
|
|
||||||
|
VITE_CLOUD_APP=https://staging-app.jetkvm.com
|
||||||
|
VITE_CLOUD_API=https://staging-api.jetkvm.com
|
|
@ -0,0 +1,21 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Print header
|
||||||
|
echo "┌──────────────────────────────────────┐"
|
||||||
|
echo "│ JetKVM Development Setup │"
|
||||||
|
echo "└──────────────────────────────────────┘"
|
||||||
|
|
||||||
|
# Prompt for IP address
|
||||||
|
printf "Please enter the IP address of your JetKVM device: "
|
||||||
|
read ip_address
|
||||||
|
|
||||||
|
# Validate input is not empty
|
||||||
|
if [ -z "$ip_address" ]; then
|
||||||
|
echo "Error: IP address cannot be empty"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Set the environment variable and run Vite
|
||||||
|
echo "Starting development server with JetKVM device at: $ip_address"
|
||||||
|
sleep 1
|
||||||
|
JETKVM_PROXY_URL="http://$ip_address" vite dev --mode=device
|
|
@ -28,6 +28,7 @@
|
||||||
<title>JetKVM</title>
|
<title>JetKVM</title>
|
||||||
<link rel="stylesheet" href="/fonts/fonts.css" />
|
<link rel="stylesheet" href="/fonts/fonts.css" />
|
||||||
<link rel="icon" href="/favicon.png" />
|
<link rel="icon" href="/favicon.png" />
|
||||||
|
%VITE_JETKVM_HEAD%
|
||||||
<script>
|
<script>
|
||||||
// Initial theme setup
|
// Initial theme setup
|
||||||
document.documentElement.classList.toggle(
|
document.documentElement.classList.toggle(
|
||||||
|
|
|
@ -8,17 +8,18 @@
|
||||||
"name": "kvm-ui",
|
"name": "kvm-ui",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@headlessui/react": "^2.1.10",
|
"@headlessui/react": "^2.2.0",
|
||||||
"@headlessui/tailwindcss": "^0.2.0",
|
"@headlessui/tailwindcss": "^0.2.1",
|
||||||
"@heroicons/react": "^2.1.3",
|
"@heroicons/react": "^2.2.0",
|
||||||
"@xterm/addon-clipboard": "^0.1.0",
|
"@xterm/addon-clipboard": "^0.1.0",
|
||||||
"@xterm/addon-fit": "^0.10.0",
|
"@xterm/addon-fit": "^0.10.0",
|
||||||
"@xterm/addon-unicode11": "^0.8.0",
|
"@xterm/addon-unicode11": "^0.8.0",
|
||||||
"@xterm/addon-web-links": "^0.11.0",
|
"@xterm/addon-web-links": "^0.11.0",
|
||||||
"@xterm/addon-webgl": "^0.18.0",
|
"@xterm/addon-webgl": "^0.18.0",
|
||||||
|
"@xterm/xterm": "^5.5.0",
|
||||||
"cva": "^1.0.0-beta.1",
|
"cva": "^1.0.0-beta.1",
|
||||||
"focus-trap-react": "^10.2.3",
|
"focus-trap-react": "^10.2.3",
|
||||||
"framer-motion": "^11.0.28",
|
"framer-motion": "^11.15.0",
|
||||||
"lodash.throttle": "^4.1.1",
|
"lodash.throttle": "^4.1.1",
|
||||||
"mini-svg-data-uri": "^1.4.4",
|
"mini-svg-data-uri": "^1.4.4",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
@ -28,32 +29,33 @@
|
||||||
"react-icons": "^5.4.0",
|
"react-icons": "^5.4.0",
|
||||||
"react-router-dom": "^6.22.3",
|
"react-router-dom": "^6.22.3",
|
||||||
"react-simple-keyboard": "^3.7.112",
|
"react-simple-keyboard": "^3.7.112",
|
||||||
"recharts": "^2.12.6",
|
"react-xtermjs": "^1.0.9",
|
||||||
"tailwind-merge": "^2.2.2",
|
"recharts": "^2.15.0",
|
||||||
|
"tailwind-merge": "^2.5.5",
|
||||||
"usehooks-ts": "^3.1.0",
|
"usehooks-ts": "^3.1.0",
|
||||||
"validator": "^13.12.0",
|
"validator": "^13.12.0",
|
||||||
"xterm": "^5.3.0",
|
"xterm": "^5.3.0",
|
||||||
"zustand": "^4.5.2"
|
"zustand": "^4.5.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/forms": "^0.5.7",
|
"@tailwindcss/forms": "^0.5.9",
|
||||||
"@tailwindcss/typography": "^0.5.12",
|
"@tailwindcss/typography": "^0.5.15",
|
||||||
"@types/react": "^18.2.66",
|
"@types/react": "^18.2.66",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
"@types/validator": "^13.12.2",
|
"@types/validator": "^13.12.2",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.2.0",
|
"@typescript-eslint/eslint-plugin": "^7.2.0",
|
||||||
"@typescript-eslint/parser": "^7.2.0",
|
"@typescript-eslint/parser": "^7.2.0",
|
||||||
"@vitejs/plugin-react-swc": "^3.5.0",
|
"@vitejs/plugin-react-swc": "^3.7.2",
|
||||||
"autoprefixer": "^10.4.19",
|
"autoprefixer": "^10.4.20",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
"eslint-plugin-react": "^7.34.1",
|
"eslint-plugin-react": "^7.34.1",
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.6",
|
"eslint-plugin-react-refresh": "^0.4.6",
|
||||||
"postcss": "^8.4.38",
|
"postcss": "^8.4.49",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.4.2",
|
||||||
"prettier-plugin-tailwindcss": "^0.5.13",
|
"prettier-plugin-tailwindcss": "^0.5.13",
|
||||||
"tailwindcss": "^3.4.3",
|
"tailwindcss": "^3.4.17",
|
||||||
"typescript": "^5.2.2",
|
"typescript": "^5.7.2",
|
||||||
"vite": "^5.2.0",
|
"vite": "^5.2.0",
|
||||||
"vite-tsconfig-paths": "^4.3.2"
|
"vite-tsconfig-paths": "^4.3.2"
|
||||||
},
|
},
|
||||||
|
@ -587,9 +589,9 @@
|
||||||
"integrity": "sha512-X8R8Oj771YRl/w+c1HqAC1szL8zWQRwFvgDwT129k9ACdBoud/+/rX9V0qiMl6LWUdP9voC2nDVZYPMQQsb6eA=="
|
"integrity": "sha512-X8R8Oj771YRl/w+c1HqAC1szL8zWQRwFvgDwT129k9ACdBoud/+/rX9V0qiMl6LWUdP9voC2nDVZYPMQQsb6eA=="
|
||||||
},
|
},
|
||||||
"node_modules/@headlessui/react": {
|
"node_modules/@headlessui/react": {
|
||||||
"version": "2.1.10",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.1.10.tgz",
|
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.0.tgz",
|
||||||
"integrity": "sha512-6mLa2fjMDAFQi+/R10B+zU3edsUk/MDtENB2zHho0lqKU1uzhAfJLUduWds4nCo8wbl3vULtC5rJfZAQ1yqIng==",
|
"integrity": "sha512-RzCEg+LXsuI7mHiSomsu/gBJSjpupm6A1qIZ5sWjd7JhARNlMiSA4kKfJpCKwU9tE+zMRterhhrP74PvfJrpXQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@floating-ui/react": "^0.26.16",
|
"@floating-ui/react": "^0.26.16",
|
||||||
"@react-aria/focus": "^3.17.1",
|
"@react-aria/focus": "^3.17.1",
|
||||||
|
@ -600,14 +602,14 @@
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "^18",
|
"react": "^18 || ^19 || ^19.0.0-rc",
|
||||||
"react-dom": "^18"
|
"react-dom": "^18 || ^19 || ^19.0.0-rc"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@headlessui/tailwindcss": {
|
"node_modules/@headlessui/tailwindcss": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@headlessui/tailwindcss/-/tailwindcss-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@headlessui/tailwindcss/-/tailwindcss-0.2.1.tgz",
|
||||||
"integrity": "sha512-fpL830Fln1SykOCboExsWr3JIVeQKieLJ3XytLe/tt1A0XzqUthOftDmjcCYLW62w7mQI7wXcoPXr3tZ9QfGxw==",
|
"integrity": "sha512-2+5+NZ+RzMyrVeCZOxdbvkUSssSxGvcUxphkIfSVLpRiKsj+/63T2TOL9dBYMXVfj/CGr6hMxSRInzXv6YY7sA==",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
},
|
},
|
||||||
|
@ -616,11 +618,11 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@heroicons/react": {
|
"node_modules/@heroicons/react": {
|
||||||
"version": "2.1.3",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz",
|
||||||
"integrity": "sha512-fEcPfo4oN345SoqdlCDdSa4ivjaKbk0jTd+oubcgNxnNgAfzysfwWfQUr+51wigiWHQQRiZNd1Ao0M5Y3M2EGg==",
|
"integrity": "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": ">= 16"
|
"react": ">= 16 || ^19.0.0-rc"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@humanwhocodes/config-array": {
|
"node_modules/@humanwhocodes/config-array": {
|
||||||
|
@ -1084,14 +1086,14 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@swc/core": {
|
"node_modules/@swc/core": {
|
||||||
"version": "1.4.12",
|
"version": "1.10.1",
|
||||||
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.4.12.tgz",
|
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.10.1.tgz",
|
||||||
"integrity": "sha512-QljRxTaUajSLB9ui93cZ38/lmThwIw/BPxjn+TphrYN6LPU3vu9/ykjgHtlpmaXDDcngL4K5i396E7iwwEUxYg==",
|
"integrity": "sha512-rQ4dS6GAdmtzKiCRt3LFVxl37FaY1cgL9kSUTnhQ2xc3fmHOd7jdJK/V4pSZMG1ruGTd0bsi34O2R0Olg9Zo/w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@swc/counter": "^0.1.2",
|
"@swc/counter": "^0.1.3",
|
||||||
"@swc/types": "^0.1.5"
|
"@swc/types": "^0.1.17"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
|
@ -1101,19 +1103,19 @@
|
||||||
"url": "https://opencollective.com/swc"
|
"url": "https://opencollective.com/swc"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@swc/core-darwin-arm64": "1.4.12",
|
"@swc/core-darwin-arm64": "1.10.1",
|
||||||
"@swc/core-darwin-x64": "1.4.12",
|
"@swc/core-darwin-x64": "1.10.1",
|
||||||
"@swc/core-linux-arm-gnueabihf": "1.4.12",
|
"@swc/core-linux-arm-gnueabihf": "1.10.1",
|
||||||
"@swc/core-linux-arm64-gnu": "1.4.12",
|
"@swc/core-linux-arm64-gnu": "1.10.1",
|
||||||
"@swc/core-linux-arm64-musl": "1.4.12",
|
"@swc/core-linux-arm64-musl": "1.10.1",
|
||||||
"@swc/core-linux-x64-gnu": "1.4.12",
|
"@swc/core-linux-x64-gnu": "1.10.1",
|
||||||
"@swc/core-linux-x64-musl": "1.4.12",
|
"@swc/core-linux-x64-musl": "1.10.1",
|
||||||
"@swc/core-win32-arm64-msvc": "1.4.12",
|
"@swc/core-win32-arm64-msvc": "1.10.1",
|
||||||
"@swc/core-win32-ia32-msvc": "1.4.12",
|
"@swc/core-win32-ia32-msvc": "1.10.1",
|
||||||
"@swc/core-win32-x64-msvc": "1.4.12"
|
"@swc/core-win32-x64-msvc": "1.10.1"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@swc/helpers": "^0.5.0"
|
"@swc/helpers": "*"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
"@swc/helpers": {
|
"@swc/helpers": {
|
||||||
|
@ -1122,9 +1124,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@swc/core-darwin-arm64": {
|
"node_modules/@swc/core-darwin-arm64": {
|
||||||
"version": "1.4.12",
|
"version": "1.10.1",
|
||||||
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.4.12.tgz",
|
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.10.1.tgz",
|
||||||
"integrity": "sha512-BZUUq91LGJsLI2BQrhYL3yARkcdN4TS3YGNS6aRYUtyeWrGCTKHL90erF2BMU2rEwZLLkOC/U899R4o4oiSHfA==",
|
"integrity": "sha512-NyELPp8EsVZtxH/mEqvzSyWpfPJ1lugpTQcSlMduZLj1EASLO4sC8wt8hmL1aizRlsbjCX+r0PyL+l0xQ64/6Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
@ -1138,9 +1140,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@swc/core-darwin-x64": {
|
"node_modules/@swc/core-darwin-x64": {
|
||||||
"version": "1.4.12",
|
"version": "1.10.1",
|
||||||
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.4.12.tgz",
|
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.10.1.tgz",
|
||||||
"integrity": "sha512-Wkk8rq1RwCOgg5ybTlfVtOYXLZATZ+QjgiBNM7pIn03A5/zZicokNTYd8L26/mifly2e74Dz34tlIZBT4aTGDA==",
|
"integrity": "sha512-L4BNt1fdQ5ZZhAk5qoDfUnXRabDOXKnXBxMDJ+PWLSxOGBbWE6aJTnu4zbGjJvtot0KM46m2LPAPY8ttknqaZA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
@ -1154,9 +1156,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@swc/core-linux-arm-gnueabihf": {
|
"node_modules/@swc/core-linux-arm-gnueabihf": {
|
||||||
"version": "1.4.12",
|
"version": "1.10.1",
|
||||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.4.12.tgz",
|
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.10.1.tgz",
|
||||||
"integrity": "sha512-8jb/SN67oTQ5KSThWlKLchhU6xnlAlnmnLCCOKK1xGtFS6vD+By9uL+qeEY2krV98UCRTf68WSmC0SLZhVoz5A==",
|
"integrity": "sha512-Y1u9OqCHgvVp2tYQAJ7hcU9qO5brDMIrA5R31rwWQIAKDkJKtv3IlTHF0hrbWk1wPR0ZdngkQSJZple7G+Grvw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
|
@ -1170,9 +1172,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@swc/core-linux-arm64-gnu": {
|
"node_modules/@swc/core-linux-arm64-gnu": {
|
||||||
"version": "1.4.12",
|
"version": "1.10.1",
|
||||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.4.12.tgz",
|
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.10.1.tgz",
|
||||||
"integrity": "sha512-DhW47DQEZKCdSq92v5F03rqdpjRXdDMqxfu4uAlZ9Uo1wJEGvY23e1SNmhji2sVHsZbBjSvoXoBLk0v00nSG8w==",
|
"integrity": "sha512-tNQHO/UKdtnqjc7o04iRXng1wTUXPgVd8Y6LI4qIbHVoVPwksZydISjMcilKNLKIwOoUQAkxyJ16SlOAeADzhQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
@ -1186,9 +1188,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@swc/core-linux-arm64-musl": {
|
"node_modules/@swc/core-linux-arm64-musl": {
|
||||||
"version": "1.4.12",
|
"version": "1.10.1",
|
||||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.4.12.tgz",
|
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.10.1.tgz",
|
||||||
"integrity": "sha512-PR57pT3TssnCRvdsaKNsxZy9N8rFg9AKA1U7W+LxbZ/7Z7PHc5PjxF0GgZpE/aLmU6xOn5VyQTlzjoamVkt05g==",
|
"integrity": "sha512-x0L2Pd9weQ6n8dI1z1Isq00VHFvpBClwQJvrt3NHzmR+1wCT/gcYl1tp9P5xHh3ldM8Cn4UjWCw+7PaUgg8FcQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
@ -1202,9 +1204,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@swc/core-linux-x64-gnu": {
|
"node_modules/@swc/core-linux-x64-gnu": {
|
||||||
"version": "1.4.12",
|
"version": "1.10.1",
|
||||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.4.12.tgz",
|
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.10.1.tgz",
|
||||||
"integrity": "sha512-HLZIWNHWuFIlH+LEmXr1lBiwGQeCshKOGcqbJyz7xpqTh7m2IPAxPWEhr/qmMTMsjluGxeIsLrcsgreTyXtgNA==",
|
"integrity": "sha512-yyYEwQcObV3AUsC79rSzN9z6kiWxKAVJ6Ntwq2N9YoZqSPYph+4/Am5fM1xEQYf/kb99csj0FgOelomJSobxQA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
@ -1218,9 +1220,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@swc/core-linux-x64-musl": {
|
"node_modules/@swc/core-linux-x64-musl": {
|
||||||
"version": "1.4.12",
|
"version": "1.10.1",
|
||||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.4.12.tgz",
|
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.10.1.tgz",
|
||||||
"integrity": "sha512-M5fBAtoOcpz2YQAFtNemrPod5BqmzAJc8pYtT3dVTn1MJllhmLHlphU8BQytvoGr1PHgJL8ZJBlBGdt70LQ7Mw==",
|
"integrity": "sha512-tcaS43Ydd7Fk7sW5ROpaf2Kq1zR+sI5K0RM+0qYLYYurvsJruj3GhBCaiN3gkzd8m/8wkqNqtVklWaQYSDsyqA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
@ -1234,9 +1236,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@swc/core-win32-arm64-msvc": {
|
"node_modules/@swc/core-win32-arm64-msvc": {
|
||||||
"version": "1.4.12",
|
"version": "1.10.1",
|
||||||
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.4.12.tgz",
|
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.10.1.tgz",
|
||||||
"integrity": "sha512-K8LjjgZ7VQFtM+eXqjfAJ0z+TKVDng3r59QYn7CL6cyxZI2brLU3lNknZcUFSouZD+gsghZI/Zb8tQjVk7aKDQ==",
|
"integrity": "sha512-D3Qo1voA7AkbOzQ2UGuKNHfYGKL6eejN8VWOoQYtGHHQi1p5KK/Q7V1ku55oxXBsj79Ny5FRMqiRJpVGad7bjQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
@ -1250,9 +1252,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@swc/core-win32-ia32-msvc": {
|
"node_modules/@swc/core-win32-ia32-msvc": {
|
||||||
"version": "1.4.12",
|
"version": "1.10.1",
|
||||||
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.4.12.tgz",
|
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.10.1.tgz",
|
||||||
"integrity": "sha512-hflO5LCxozngoOmiQbDPyvt6ODc5Cu9AwTJP9uH/BSMPdEQ6PCnefuUOJLAKew2q9o+NmDORuJk+vgqQz9Uzpg==",
|
"integrity": "sha512-WalYdFoU3454Og+sDKHM1MrjvxUGwA2oralknXkXL8S0I/8RkWZOB++p3pLaGbTvOO++T+6znFbQdR8KRaa7DA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
|
@ -1266,9 +1268,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@swc/core-win32-x64-msvc": {
|
"node_modules/@swc/core-win32-x64-msvc": {
|
||||||
"version": "1.4.12",
|
"version": "1.10.1",
|
||||||
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.4.12.tgz",
|
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.10.1.tgz",
|
||||||
"integrity": "sha512-3A4qMtddBDbtprV5edTB/SgJn9L+X5TL7RGgS3eWtEgn/NG8gA80X/scjf1v2MMeOsrcxiYhnemI2gXCKuQN2g==",
|
"integrity": "sha512-JWobfQDbTnoqaIwPKQ3DVSywihVXlQMbDuwik/dDWlj33A8oEHcjPOGs4OqcA3RHv24i+lfCQpM3Mn4FAMfacA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
@ -1296,30 +1298,30 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@swc/types": {
|
"node_modules/@swc/types": {
|
||||||
"version": "0.1.6",
|
"version": "0.1.17",
|
||||||
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.17.tgz",
|
||||||
"integrity": "sha512-/JLo/l2JsT/LRd80C3HfbmVpxOAJ11FO2RCEslFrgzLltoP9j8XIbsyDcfCt2WWyX+CM96rBoNM+IToAkFOugg==",
|
"integrity": "sha512-V5gRru+aD8YVyCOMAjMpWR1Ui577DD5KSJsHP8RAxopAH22jFz6GZd/qxqjO6MJHQhcsjvjOFXyDhyLQUnMveQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@swc/counter": "^0.1.3"
|
"@swc/counter": "^0.1.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/forms": {
|
"node_modules/@tailwindcss/forms": {
|
||||||
"version": "0.5.7",
|
"version": "0.5.9",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.7.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.9.tgz",
|
||||||
"integrity": "sha512-QE7X69iQI+ZXwldE+rzasvbJiyV/ju1FGHH0Qn2W3FKbuYtqp8LKcy6iSw79fVUT5/Vvf+0XgLCeYVG+UV6hOw==",
|
"integrity": "sha512-tM4XVr2+UVTxXJzey9Twx48c1gcxFStqn1pQz0tRsX8o3DvxhN5oY5pvyAbUx7VTaZxpej4Zzvc6h+1RJBzpIg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"mini-svg-data-uri": "^1.2.3"
|
"mini-svg-data-uri": "^1.2.3"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1"
|
"tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/typography": {
|
"node_modules/@tailwindcss/typography": {
|
||||||
"version": "0.5.12",
|
"version": "0.5.15",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.12.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.15.tgz",
|
||||||
"integrity": "sha512-CNwpBpconcP7ppxmuq3qvaCxiRWnbhANpY/ruH4L5qs2GCiVDJXde/pjj2HWPV1+Q4G9+V/etrwUYopdcjAlyg==",
|
"integrity": "sha512-AqhlCXl+8grUz8uqExv5OTtgpjuVIwFTSXTrh8y9/pw6q2ek7fJ+Y8ZEVw7EB2DCcuCOtEjf9w3+J3rzts01uA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lodash.castarray": "^4.4.0",
|
"lodash.castarray": "^4.4.0",
|
||||||
|
@ -1328,7 +1330,7 @@
|
||||||
"postcss-selector-parser": "6.0.10"
|
"postcss-selector-parser": "6.0.10"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"tailwindcss": ">=3.0.0 || insiders"
|
"tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/typography/node_modules/postcss-selector-parser": {
|
"node_modules/@tailwindcss/typography/node_modules/postcss-selector-parser": {
|
||||||
|
@ -1669,15 +1671,15 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@vitejs/plugin-react-swc": {
|
"node_modules/@vitejs/plugin-react-swc": {
|
||||||
"version": "3.6.0",
|
"version": "3.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.7.2.tgz",
|
||||||
"integrity": "sha512-XFRbsGgpGxGzEV5i5+vRiro1bwcIaZDIdBRP16qwm+jP68ue/S8FJTBEgOeojtVDYrbSua3XFp71kC8VJE6v+g==",
|
"integrity": "sha512-y0byko2b2tSVVf5Gpng1eEhX1OvPC7x8yns1Fx8jDzlJp4LS6CMkCPfLw47cjyoMrshQDoQw4qcgjsU9VvlCew==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@swc/core": "^1.3.107"
|
"@swc/core": "^1.7.26"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"vite": "^4 || ^5"
|
"vite": "^4 || ^5 || ^6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@xterm/addon-clipboard": {
|
"node_modules/@xterm/addon-clipboard": {
|
||||||
|
@ -1726,8 +1728,7 @@
|
||||||
"node_modules/@xterm/xterm": {
|
"node_modules/@xterm/xterm": {
|
||||||
"version": "5.5.0",
|
"version": "5.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
|
||||||
"integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==",
|
"integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A=="
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"version": "8.11.3",
|
"version": "8.11.3",
|
||||||
|
@ -1965,9 +1966,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/autoprefixer": {
|
"node_modules/autoprefixer": {
|
||||||
"version": "10.4.19",
|
"version": "10.4.20",
|
||||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz",
|
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz",
|
||||||
"integrity": "sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==",
|
"integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
@ -1984,11 +1985,11 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"browserslist": "^4.23.0",
|
"browserslist": "^4.23.3",
|
||||||
"caniuse-lite": "^1.0.30001599",
|
"caniuse-lite": "^1.0.30001646",
|
||||||
"fraction.js": "^4.3.7",
|
"fraction.js": "^4.3.7",
|
||||||
"normalize-range": "^0.1.2",
|
"normalize-range": "^0.1.2",
|
||||||
"picocolors": "^1.0.0",
|
"picocolors": "^1.0.1",
|
||||||
"postcss-value-parser": "^4.2.0"
|
"postcss-value-parser": "^4.2.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
|
@ -2041,20 +2042,20 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/braces": {
|
"node_modules/braces": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||||
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fill-range": "^7.0.1"
|
"fill-range": "^7.1.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/browserslist": {
|
"node_modules/browserslist": {
|
||||||
"version": "4.23.0",
|
"version": "4.24.3",
|
||||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz",
|
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.3.tgz",
|
||||||
"integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==",
|
"integrity": "sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
@ -2071,10 +2072,10 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"caniuse-lite": "^1.0.30001587",
|
"caniuse-lite": "^1.0.30001688",
|
||||||
"electron-to-chromium": "^1.4.668",
|
"electron-to-chromium": "^1.5.73",
|
||||||
"node-releases": "^2.0.14",
|
"node-releases": "^2.0.19",
|
||||||
"update-browserslist-db": "^1.0.13"
|
"update-browserslist-db": "^1.1.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"browserslist": "cli.js"
|
"browserslist": "cli.js"
|
||||||
|
@ -2120,9 +2121,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001666",
|
"version": "1.0.30001690",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001666.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz",
|
||||||
"integrity": "sha512-gD14ICmoV5ZZM1OdzPWmpx+q4GyefaK06zi8hmfHV5xe4/2nOQX3+Dw5o+fSqOws2xVwL9j+anOPFwHzdEdV4g==",
|
"integrity": "sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
@ -2547,9 +2548,9 @@
|
||||||
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="
|
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="
|
||||||
},
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.4.729",
|
"version": "1.5.75",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.729.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.75.tgz",
|
||||||
"integrity": "sha512-bx7+5Saea/qu14kmPTDHQxkp2UnziG3iajUQu3BxFvCOnpAJdDbMV4rSl+EqFDkkpNNVUFlR1kDfpL59xfy1HA==",
|
"integrity": "sha512-Lf3++DumRE/QmweGjU+ZcKqQ+3bKkU/qjaKYhIJKEOhgIO9Xs6IiAQFkfFoj+RhgDk4LUeNsLo6plExHqSyu6Q==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/emoji-regex": {
|
"node_modules/emoji-regex": {
|
||||||
|
@ -2754,9 +2755,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/escalade": {
|
"node_modules/escalade": {
|
||||||
"version": "3.1.2",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||||||
"integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==",
|
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
|
@ -3112,9 +3113,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/fill-range": {
|
"node_modules/fill-range": {
|
||||||
"version": "7.0.1",
|
"version": "7.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||||
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"to-regex-range": "^5.0.1"
|
"to-regex-range": "^5.0.1"
|
||||||
},
|
},
|
||||||
|
@ -3218,16 +3219,18 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/framer-motion": {
|
"node_modules/framer-motion": {
|
||||||
"version": "11.0.28",
|
"version": "11.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.0.28.tgz",
|
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.15.0.tgz",
|
||||||
"integrity": "sha512-j/vNYTCH5MX5sY/3dwMs00z1+qAqKX3iIHF762bwqlU814ooD5dDbuj3pA0LmIT5YqyryCkXEb/q+zRblin0lw==",
|
"integrity": "sha512-MLk8IvZntxOMg7lDBLw2qgTHHv664bYoYmnFTmE0Gm/FW67aOJk0WM3ctMcG+Xhcv+vh5uyyXwxvxhSeJzSe+w==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"motion-dom": "^11.14.3",
|
||||||
|
"motion-utils": "^11.14.3",
|
||||||
"tslib": "^2.4.0"
|
"tslib": "^2.4.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@emotion/is-prop-valid": "*",
|
"@emotion/is-prop-valid": "*",
|
||||||
"react": "^18.0.0",
|
"react": "^18.0.0 || ^19.0.0",
|
||||||
"react-dom": "^18.0.0"
|
"react-dom": "^18.0.0 || ^19.0.0"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
"@emotion/is-prop-valid": {
|
"@emotion/is-prop-valid": {
|
||||||
|
@ -4021,9 +4024,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/jiti": {
|
"node_modules/jiti": {
|
||||||
"version": "1.21.0",
|
"version": "1.21.7",
|
||||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
|
||||||
"integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==",
|
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
|
||||||
"bin": {
|
"bin": {
|
||||||
"jiti": "bin/jiti.js"
|
"jiti": "bin/jiti.js"
|
||||||
}
|
}
|
||||||
|
@ -4106,11 +4109,14 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lilconfig": {
|
"node_modules/lilconfig": {
|
||||||
"version": "2.1.0",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
|
||||||
"integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==",
|
"integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=14"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/antonk52"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lines-and-columns": {
|
"node_modules/lines-and-columns": {
|
||||||
|
@ -4198,11 +4204,11 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/micromatch": {
|
"node_modules/micromatch": {
|
||||||
"version": "4.0.5",
|
"version": "4.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||||
"integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
|
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"braces": "^3.0.2",
|
"braces": "^3.0.3",
|
||||||
"picomatch": "^2.3.1"
|
"picomatch": "^2.3.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
@ -4239,6 +4245,16 @@
|
||||||
"node": ">=16 || 14 >=14.17"
|
"node": ">=16 || 14 >=14.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/motion-dom": {
|
||||||
|
"version": "11.14.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.14.3.tgz",
|
||||||
|
"integrity": "sha512-lW+D2wBy5vxLJi6aCP0xyxTxlTfiu+b+zcpVbGVFUxotwThqhdpPRSmX8xztAgtZMPMeU0WGVn/k1w4I+TbPqA=="
|
||||||
|
},
|
||||||
|
"node_modules/motion-utils": {
|
||||||
|
"version": "11.14.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-11.14.3.tgz",
|
||||||
|
"integrity": "sha512-Xg+8xnqIJTpr0L/cidfTTBFkvRw26ZtGGuIhA94J9PQ2p4mEa06Xx7QVYZH0BP+EpMSaDlu+q0I0mmvwADPsaQ=="
|
||||||
|
},
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||||
|
@ -4279,9 +4295,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/node-releases": {
|
"node_modules/node-releases": {
|
||||||
"version": "2.0.14",
|
"version": "2.0.19",
|
||||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
|
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
|
||||||
"integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==",
|
"integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/normalize-path": {
|
"node_modules/normalize-path": {
|
||||||
|
@ -4551,9 +4567,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/picocolors": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.0.0",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
|
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
|
||||||
},
|
},
|
||||||
"node_modules/picomatch": {
|
"node_modules/picomatch": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
|
@ -4592,9 +4608,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.4.38",
|
"version": "8.4.49",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz",
|
||||||
"integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==",
|
"integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
|
@ -4611,8 +4627,8 @@
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.7",
|
"nanoid": "^3.3.7",
|
||||||
"picocolors": "^1.0.0",
|
"picocolors": "^1.1.1",
|
||||||
"source-map-js": "^1.2.0"
|
"source-map-js": "^1.2.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^10 || ^12 || >=14"
|
"node": "^10 || ^12 || >=14"
|
||||||
|
@ -4702,39 +4718,34 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postcss-load-config/node_modules/lilconfig": {
|
|
||||||
"version": "3.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.1.tgz",
|
|
||||||
"integrity": "sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/antonk52"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/postcss-nested": {
|
"node_modules/postcss-nested": {
|
||||||
"version": "6.0.1",
|
"version": "6.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
|
||||||
"integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==",
|
"integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/postcss/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ai"
|
||||||
|
}
|
||||||
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"postcss-selector-parser": "^6.0.11"
|
"postcss-selector-parser": "^6.1.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.0"
|
"node": ">=12.0"
|
||||||
},
|
},
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/postcss/"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"postcss": "^8.2.14"
|
"postcss": "^8.2.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postcss-selector-parser": {
|
"node_modules/postcss-selector-parser": {
|
||||||
"version": "6.0.16",
|
"version": "6.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz",
|
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
|
||||||
"integrity": "sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==",
|
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cssesc": "^3.0.0",
|
"cssesc": "^3.0.0",
|
||||||
"util-deprecate": "^1.0.2"
|
"util-deprecate": "^1.0.2"
|
||||||
|
@ -4758,9 +4769,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prettier": {
|
"node_modules/prettier": {
|
||||||
"version": "3.2.5",
|
"version": "3.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz",
|
||||||
"integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==",
|
"integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"prettier": "bin/prettier.cjs"
|
"prettier": "bin/prettier.cjs"
|
||||||
|
@ -5015,6 +5026,14 @@
|
||||||
"react-dom": ">=16.6.0"
|
"react-dom": ">=16.6.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-xtermjs": {
|
||||||
|
"version": "1.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-xtermjs/-/react-xtermjs-1.0.9.tgz",
|
||||||
|
"integrity": "sha512-lrK1xiWfgxAC+4shtMHh0Irxg2t5t7JbTtpP0W7GIf1gQ9SHW/djmyiLpQSA75mN1DpT0bKeqj1fOKd0XX8RBA==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@xterm/xterm": "^5.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/read-cache": {
|
"node_modules/read-cache": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||||
|
@ -5035,14 +5054,14 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/recharts": {
|
"node_modules/recharts": {
|
||||||
"version": "2.12.6",
|
"version": "2.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/recharts/-/recharts-2.12.6.tgz",
|
"resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.0.tgz",
|
||||||
"integrity": "sha512-D+7j9WI+D0NHauah3fKHuNNcRK8bOypPW7os1DERinogGBGaHI7i6tQKJ0aUF3JXyBZ63dyfKIW2WTOPJDxJ8w==",
|
"integrity": "sha512-cIvMxDfpAmqAmVgc4yb7pgm/O1tmmkl/CjrvXuW+62/+7jj/iF9Ykm+hb/UJt42TREHMyd3gb+pkgoa2MxgDIw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"clsx": "^2.0.0",
|
"clsx": "^2.0.0",
|
||||||
"eventemitter3": "^4.0.1",
|
"eventemitter3": "^4.0.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"react-is": "^16.10.2",
|
"react-is": "^18.3.1",
|
||||||
"react-smooth": "^4.0.0",
|
"react-smooth": "^4.0.0",
|
||||||
"recharts-scale": "^0.4.4",
|
"recharts-scale": "^0.4.4",
|
||||||
"tiny-invariant": "^1.3.1",
|
"tiny-invariant": "^1.3.1",
|
||||||
|
@ -5052,8 +5071,8 @@
|
||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "^16.0.0 || ^17.0.0 || ^18.0.0",
|
"react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||||
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0"
|
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/recharts-scale": {
|
"node_modules/recharts-scale": {
|
||||||
|
@ -5064,6 +5083,11 @@
|
||||||
"decimal.js-light": "^2.4.1"
|
"decimal.js-light": "^2.4.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/recharts/node_modules/react-is": {
|
||||||
|
"version": "18.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
|
||||||
|
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="
|
||||||
|
},
|
||||||
"node_modules/reflect.getprototypeof": {
|
"node_modules/reflect.getprototypeof": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz",
|
||||||
|
@ -5362,9 +5386,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/source-map-js": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
"integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
|
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
|
@ -5610,44 +5634,41 @@
|
||||||
"integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew=="
|
"integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew=="
|
||||||
},
|
},
|
||||||
"node_modules/tailwind-merge": {
|
"node_modules/tailwind-merge": {
|
||||||
"version": "2.2.2",
|
"version": "2.5.5",
|
||||||
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.5.tgz",
|
||||||
"integrity": "sha512-tWANXsnmJzgw6mQ07nE3aCDkCK4QdT3ThPMCzawoYA2Pws7vSTCvz3Vrjg61jVUGfFZPJzxEP+NimbcW+EdaDw==",
|
"integrity": "sha512-0LXunzzAZzo0tEPxV3I297ffKZPlKDrjj7NXphC8V5ak9yHC5zRmxnOe2m/Rd/7ivsOMJe3JZ2JVocoDdQTRBA==",
|
||||||
"dependencies": {
|
|
||||||
"@babel/runtime": "^7.24.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "github",
|
"type": "github",
|
||||||
"url": "https://github.com/sponsors/dcastil"
|
"url": "https://github.com/sponsors/dcastil"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tailwindcss": {
|
"node_modules/tailwindcss": {
|
||||||
"version": "3.4.3",
|
"version": "3.4.17",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
|
||||||
"integrity": "sha512-U7sxQk/n397Bmx4JHbJx/iSOOv5G+II3f1kpLpY2QeUv5DcPdcTsYLlusZfq1NthHS1c1cZoyFmmkex1rzke0A==",
|
"integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@alloc/quick-lru": "^5.2.0",
|
"@alloc/quick-lru": "^5.2.0",
|
||||||
"arg": "^5.0.2",
|
"arg": "^5.0.2",
|
||||||
"chokidar": "^3.5.3",
|
"chokidar": "^3.6.0",
|
||||||
"didyoumean": "^1.2.2",
|
"didyoumean": "^1.2.2",
|
||||||
"dlv": "^1.1.3",
|
"dlv": "^1.1.3",
|
||||||
"fast-glob": "^3.3.0",
|
"fast-glob": "^3.3.2",
|
||||||
"glob-parent": "^6.0.2",
|
"glob-parent": "^6.0.2",
|
||||||
"is-glob": "^4.0.3",
|
"is-glob": "^4.0.3",
|
||||||
"jiti": "^1.21.0",
|
"jiti": "^1.21.6",
|
||||||
"lilconfig": "^2.1.0",
|
"lilconfig": "^3.1.3",
|
||||||
"micromatch": "^4.0.5",
|
"micromatch": "^4.0.8",
|
||||||
"normalize-path": "^3.0.0",
|
"normalize-path": "^3.0.0",
|
||||||
"object-hash": "^3.0.0",
|
"object-hash": "^3.0.0",
|
||||||
"picocolors": "^1.0.0",
|
"picocolors": "^1.1.1",
|
||||||
"postcss": "^8.4.23",
|
"postcss": "^8.4.47",
|
||||||
"postcss-import": "^15.1.0",
|
"postcss-import": "^15.1.0",
|
||||||
"postcss-js": "^4.0.1",
|
"postcss-js": "^4.0.1",
|
||||||
"postcss-load-config": "^4.0.1",
|
"postcss-load-config": "^4.0.2",
|
||||||
"postcss-nested": "^6.0.1",
|
"postcss-nested": "^6.2.0",
|
||||||
"postcss-selector-parser": "^6.0.11",
|
"postcss-selector-parser": "^6.1.2",
|
||||||
"resolve": "^1.22.2",
|
"resolve": "^1.22.8",
|
||||||
"sucrase": "^3.32.0"
|
"sucrase": "^3.35.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"tailwind": "lib/cli.js",
|
"tailwind": "lib/cli.js",
|
||||||
|
@ -5854,9 +5875,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "5.4.4",
|
"version": "5.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.4.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz",
|
||||||
"integrity": "sha512-dGE2Vv8cpVvw28v8HCPqyb08EzbBURxDpuhJvTrusShUfGnhHBafDsLdS1EhhxyL6BJQE+2cT3dDPAv+MQ6oLw==",
|
"integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
|
@ -5882,9 +5903,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/update-browserslist-db": {
|
"node_modules/update-browserslist-db": {
|
||||||
"version": "1.0.13",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz",
|
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz",
|
||||||
"integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==",
|
"integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
@ -5901,8 +5922,8 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"escalade": "^3.1.1",
|
"escalade": "^3.2.0",
|
||||||
"picocolors": "^1.0.0"
|
"picocolors": "^1.1.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"update-browserslist-db": "cli.js"
|
"update-browserslist-db": "cli.js"
|
||||||
|
|
|
@ -7,24 +7,28 @@
|
||||||
"node": "21.1.0"
|
"node": "21.1.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev --mode=development",
|
"dev": "./dev_device.sh",
|
||||||
|
"dev:cloud": "vite dev --mode=development",
|
||||||
"build": "npm run build:prod",
|
"build": "npm run build:prod",
|
||||||
"build:device": "tsc && vite build --mode=device --emptyOutDir",
|
"build:device": "tsc && vite build --mode=device --emptyOutDir",
|
||||||
|
"build:staging": "tsc && vite build --mode=staging",
|
||||||
"build:prod": "tsc && vite build --mode=production",
|
"build:prod": "tsc && vite build --mode=production",
|
||||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0"
|
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||||
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@headlessui/react": "^2.1.10",
|
"@headlessui/react": "^2.2.0",
|
||||||
"@headlessui/tailwindcss": "^0.2.0",
|
"@headlessui/tailwindcss": "^0.2.1",
|
||||||
"@heroicons/react": "^2.1.3",
|
"@heroicons/react": "^2.2.0",
|
||||||
"@xterm/addon-clipboard": "^0.1.0",
|
"@xterm/addon-clipboard": "^0.1.0",
|
||||||
"@xterm/addon-fit": "^0.10.0",
|
"@xterm/addon-fit": "^0.10.0",
|
||||||
"@xterm/addon-unicode11": "^0.8.0",
|
"@xterm/addon-unicode11": "^0.8.0",
|
||||||
"@xterm/addon-web-links": "^0.11.0",
|
"@xterm/addon-web-links": "^0.11.0",
|
||||||
"@xterm/addon-webgl": "^0.18.0",
|
"@xterm/addon-webgl": "^0.18.0",
|
||||||
|
"@xterm/xterm": "^5.5.0",
|
||||||
"cva": "^1.0.0-beta.1",
|
"cva": "^1.0.0-beta.1",
|
||||||
"focus-trap-react": "^10.2.3",
|
"focus-trap-react": "^10.2.3",
|
||||||
"framer-motion": "^11.0.28",
|
"framer-motion": "^11.15.0",
|
||||||
"lodash.throttle": "^4.1.1",
|
"lodash.throttle": "^4.1.1",
|
||||||
"mini-svg-data-uri": "^1.4.4",
|
"mini-svg-data-uri": "^1.4.4",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
@ -34,32 +38,33 @@
|
||||||
"react-icons": "^5.4.0",
|
"react-icons": "^5.4.0",
|
||||||
"react-router-dom": "^6.22.3",
|
"react-router-dom": "^6.22.3",
|
||||||
"react-simple-keyboard": "^3.7.112",
|
"react-simple-keyboard": "^3.7.112",
|
||||||
"recharts": "^2.12.6",
|
"react-xtermjs": "^1.0.9",
|
||||||
"tailwind-merge": "^2.2.2",
|
"recharts": "^2.15.0",
|
||||||
|
"tailwind-merge": "^2.5.5",
|
||||||
"usehooks-ts": "^3.1.0",
|
"usehooks-ts": "^3.1.0",
|
||||||
"validator": "^13.12.0",
|
"validator": "^13.12.0",
|
||||||
"xterm": "^5.3.0",
|
"xterm": "^5.3.0",
|
||||||
"zustand": "^4.5.2"
|
"zustand": "^4.5.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/forms": "^0.5.7",
|
"@tailwindcss/forms": "^0.5.9",
|
||||||
"@tailwindcss/typography": "^0.5.12",
|
"@tailwindcss/typography": "^0.5.15",
|
||||||
"@types/react": "^18.2.66",
|
"@types/react": "^18.2.66",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
"@types/validator": "^13.12.2",
|
"@types/validator": "^13.12.2",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.2.0",
|
"@typescript-eslint/eslint-plugin": "^7.2.0",
|
||||||
"@typescript-eslint/parser": "^7.2.0",
|
"@typescript-eslint/parser": "^7.2.0",
|
||||||
"@vitejs/plugin-react-swc": "^3.5.0",
|
"@vitejs/plugin-react-swc": "^3.7.2",
|
||||||
"autoprefixer": "^10.4.19",
|
"autoprefixer": "^10.4.20",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
"eslint-plugin-react": "^7.34.1",
|
"eslint-plugin-react": "^7.34.1",
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.6",
|
"eslint-plugin-react-refresh": "^0.4.6",
|
||||||
"postcss": "^8.4.38",
|
"postcss": "^8.4.49",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.4.2",
|
||||||
"prettier-plugin-tailwindcss": "^0.5.13",
|
"prettier-plugin-tailwindcss": "^0.5.13",
|
||||||
"tailwindcss": "^3.4.3",
|
"tailwindcss": "^3.4.17",
|
||||||
"typescript": "^5.2.2",
|
"typescript": "^5.7.2",
|
||||||
"vite": "^5.2.0",
|
"vite": "^5.2.0",
|
||||||
"vite-tsconfig-paths": "^4.3.2"
|
"vite-tsconfig-paths": "^4.3.2"
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,12 +2,12 @@ import { Button } from "@components/Button";
|
||||||
import {
|
import {
|
||||||
useHidStore,
|
useHidStore,
|
||||||
useMountMediaStore,
|
useMountMediaStore,
|
||||||
useUiStore,
|
|
||||||
useSettingsStore,
|
useSettingsStore,
|
||||||
|
useUiStore,
|
||||||
} from "@/hooks/stores";
|
} from "@/hooks/stores";
|
||||||
import { MdOutlineContentPasteGo } from "react-icons/md";
|
import { MdOutlineContentPasteGo } from "react-icons/md";
|
||||||
import Container from "@components/Container";
|
import Container from "@components/Container";
|
||||||
import { LuHardDrive, LuMaximize, LuSettings, LuSignal } from "react-icons/lu";
|
import { LuCable, LuHardDrive, LuMaximize, LuSettings, LuSignal } from "react-icons/lu";
|
||||||
import { cx } from "@/cva.config";
|
import { cx } from "@/cva.config";
|
||||||
import PasteModal from "@/components/popovers/PasteModal";
|
import PasteModal from "@/components/popovers/PasteModal";
|
||||||
import { FaKeyboard } from "react-icons/fa6";
|
import { FaKeyboard } from "react-icons/fa6";
|
||||||
|
@ -16,6 +16,7 @@ import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
|
||||||
import MountPopopover from "./popovers/MountPopover";
|
import MountPopopover from "./popovers/MountPopover";
|
||||||
import { Fragment, useCallback, useRef } from "react";
|
import { Fragment, useCallback, useRef } from "react";
|
||||||
import { CommandLineIcon } from "@heroicons/react/20/solid";
|
import { CommandLineIcon } from "@heroicons/react/20/solid";
|
||||||
|
import ExtensionPopover from "./popovers/ExtensionPopover";
|
||||||
|
|
||||||
export default function Actionbar({
|
export default function Actionbar({
|
||||||
requestFullscreen,
|
requestFullscreen,
|
||||||
|
@ -27,8 +28,8 @@ export default function Actionbar({
|
||||||
const setVirtualKeyboard = useHidStore(state => state.setVirtualKeyboardEnabled);
|
const setVirtualKeyboard = useHidStore(state => state.setVirtualKeyboardEnabled);
|
||||||
const toggleSidebarView = useUiStore(state => state.toggleSidebarView);
|
const toggleSidebarView = useUiStore(state => state.toggleSidebarView);
|
||||||
const setDisableFocusTrap = useUiStore(state => state.setDisableVideoFocusTrap);
|
const setDisableFocusTrap = useUiStore(state => state.setDisableVideoFocusTrap);
|
||||||
const enableTerminal = useUiStore(state => state.enableTerminal);
|
const terminalType = useUiStore(state => state.terminalType);
|
||||||
const setEnableTerminal = useUiStore(state => state.setEnableTerminal);
|
const setTerminalType = useUiStore(state => state.setTerminalType);
|
||||||
const remoteVirtualMediaState = useMountMediaStore(
|
const remoteVirtualMediaState = useMountMediaStore(
|
||||||
state => state.remoteVirtualMediaState,
|
state => state.remoteVirtualMediaState,
|
||||||
);
|
);
|
||||||
|
@ -53,7 +54,7 @@ export default function Actionbar({
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container className="bg-white border-b border-b-slate-800/20 dark:bg-slate-900 dark:border-b-slate-300/20">
|
<Container className="border-b border-b-slate-800/20 bg-white dark:border-b-slate-300/20 dark:bg-slate-900">
|
||||||
<div
|
<div
|
||||||
onKeyUp={e => e.stopPropagation()}
|
onKeyUp={e => e.stopPropagation()}
|
||||||
onKeyDown={e => e.stopPropagation()}
|
onKeyDown={e => e.stopPropagation()}
|
||||||
|
@ -66,7 +67,7 @@ export default function Actionbar({
|
||||||
theme="light"
|
theme="light"
|
||||||
text="Web Terminal"
|
text="Web Terminal"
|
||||||
LeadingIcon={({ className }) => <CommandLineIcon className={className} />}
|
LeadingIcon={({ className }) => <CommandLineIcon className={className} />}
|
||||||
onClick={() => setEnableTerminal(!enableTerminal)}
|
onClick={() => setTerminalType(terminalType === "kvm" ? "none" : "kvm")}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Popover>
|
<Popover>
|
||||||
|
@ -92,7 +93,7 @@ export default function Actionbar({
|
||||||
{({ open }) => {
|
{({ open }) => {
|
||||||
checkIfStateChanged(open);
|
checkIfStateChanged(open);
|
||||||
return (
|
return (
|
||||||
<div className="w-full max-w-xl mx-auto">
|
<div className="mx-auto w-full max-w-xl">
|
||||||
<PasteModal />
|
<PasteModal />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -134,7 +135,7 @@ export default function Actionbar({
|
||||||
{({ open }) => {
|
{({ open }) => {
|
||||||
checkIfStateChanged(open);
|
checkIfStateChanged(open);
|
||||||
return (
|
return (
|
||||||
<div className="w-full max-w-xl mx-auto">
|
<div className="mx-auto w-full max-w-xl">
|
||||||
<MountPopopover />
|
<MountPopopover />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -186,7 +187,7 @@ export default function Actionbar({
|
||||||
{({ open }) => {
|
{({ open }) => {
|
||||||
checkIfStateChanged(open);
|
checkIfStateChanged(open);
|
||||||
return (
|
return (
|
||||||
<div className="w-full max-w-xl mx-auto">
|
<div className="mx-auto w-full max-w-xl">
|
||||||
<WakeOnLanModal />
|
<WakeOnLanModal />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -206,6 +207,33 @@ export default function Actionbar({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-wrap items-center gap-x-2 gap-y-2">
|
<div className="flex flex-wrap items-center gap-x-2 gap-y-2">
|
||||||
|
<Popover>
|
||||||
|
<PopoverButton as={Fragment}>
|
||||||
|
<Button
|
||||||
|
size="XS"
|
||||||
|
theme="light"
|
||||||
|
text="Extension"
|
||||||
|
LeadingIcon={LuCable}
|
||||||
|
onClick={() => {
|
||||||
|
setDisableFocusTrap(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</PopoverButton>
|
||||||
|
<PopoverPanel
|
||||||
|
anchor="bottom start"
|
||||||
|
transition
|
||||||
|
className={cx(
|
||||||
|
"z-10 flex w-[420px] flex-col !overflow-visible",
|
||||||
|
"flex origin-top flex-col transition duration-300 ease-out data-[closed]:translate-y-8 data-[closed]:opacity-0",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{({ open }) => {
|
||||||
|
checkIfStateChanged(open);
|
||||||
|
return <ExtensionPopover />;
|
||||||
|
}}
|
||||||
|
</PopoverPanel>
|
||||||
|
</Popover>
|
||||||
|
|
||||||
<div className="block lg:hidden">
|
<div className="block lg:hidden">
|
||||||
<Button
|
<Button
|
||||||
size="XS"
|
size="XS"
|
||||||
|
@ -241,7 +269,7 @@ export default function Actionbar({
|
||||||
onClick={() => toggleSidebarView("system")}
|
onClick={() => toggleSidebarView("system")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="items-center hidden gap-x-2 lg:flex">
|
<div className="hidden items-center gap-x-2 lg:flex">
|
||||||
<div className="h-4 w-[1px] bg-slate-300 dark:bg-slate-600" />
|
<div className="h-4 w-[1px] bg-slate-300 dark:bg-slate-600" />
|
||||||
<Button
|
<Button
|
||||||
size="XS"
|
size="XS"
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { useLocation, useNavigation, useSearchParams } from "react-router-dom";
|
||||||
import Fieldset from "@components/Fieldset";
|
import Fieldset from "@components/Fieldset";
|
||||||
import GridBackground from "@components/GridBackground";
|
import GridBackground from "@components/GridBackground";
|
||||||
import StepCounter from "@components/StepCounter";
|
import StepCounter from "@components/StepCounter";
|
||||||
|
import { CLOUD_API } from "@/ui.config";
|
||||||
|
|
||||||
type AuthLayoutProps = {
|
type AuthLayoutProps = {
|
||||||
title: string;
|
title: string;
|
||||||
|
@ -62,7 +63,7 @@ export default function AuthLayout({
|
||||||
<Fieldset className="space-y-12">
|
<Fieldset className="space-y-12">
|
||||||
<div className="max-w-sm mx-auto space-y-4">
|
<div className="max-w-sm mx-auto space-y-4">
|
||||||
<form
|
<form
|
||||||
action={`${import.meta.env.VITE_CLOUD_API}/oidc/google`}
|
action={`${CLOUD_API}/oidc/google`}
|
||||||
method="POST"
|
method="POST"
|
||||||
>
|
>
|
||||||
{/*This could be the KVM ID*/}
|
{/*This could be the KVM ID*/}
|
||||||
|
|
|
@ -156,7 +156,16 @@ function ButtonContent(props: ButtonContentPropsType) {
|
||||||
|
|
||||||
type ButtonPropsType = Pick<
|
type ButtonPropsType = Pick<
|
||||||
JSX.IntrinsicElements["button"],
|
JSX.IntrinsicElements["button"],
|
||||||
"type" | "disabled" | "onClick" | "name" | "value" | "formNoValidate" | "onMouseLeave"
|
| "type"
|
||||||
|
| "disabled"
|
||||||
|
| "onClick"
|
||||||
|
| "name"
|
||||||
|
| "value"
|
||||||
|
| "formNoValidate"
|
||||||
|
| "onMouseLeave"
|
||||||
|
| "onMouseDown"
|
||||||
|
| "onMouseUp"
|
||||||
|
| "onMouseLeave"
|
||||||
> &
|
> &
|
||||||
React.ComponentProps<typeof ButtonContent> & {
|
React.ComponentProps<typeof ButtonContent> & {
|
||||||
fetcher?: FetcherWithComponents<unknown>;
|
fetcher?: FetcherWithComponents<unknown>;
|
||||||
|
@ -179,6 +188,9 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonPropsType>(
|
||||||
type={type}
|
type={type}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
|
onMouseDown={props?.onMouseDown}
|
||||||
|
onMouseUp={props?.onMouseUp}
|
||||||
|
onMouseLeave={props?.onMouseLeave}
|
||||||
name={props.name}
|
name={props.name}
|
||||||
value={props.value}
|
value={props.value}
|
||||||
>
|
>
|
||||||
|
|
|
@ -14,6 +14,7 @@ import PeerConnectionStatusCard from "@components/PeerConnectionStatusCard";
|
||||||
import api from "../api";
|
import api from "../api";
|
||||||
import { isOnDevice } from "../main";
|
import { isOnDevice } from "../main";
|
||||||
import { Button, LinkButton } from "./Button";
|
import { Button, LinkButton } from "./Button";
|
||||||
|
import { CLOUD_API, SIGNAL_API } from "@/ui.config";
|
||||||
|
|
||||||
interface NavbarProps {
|
interface NavbarProps {
|
||||||
isLoggedIn: boolean;
|
isLoggedIn: boolean;
|
||||||
|
@ -37,8 +38,8 @@ export default function DashboardNavbar({
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const onLogout = useCallback(async () => {
|
const onLogout = useCallback(async () => {
|
||||||
const logoutUrl = isOnDevice
|
const logoutUrl = isOnDevice
|
||||||
? `${import.meta.env.VITE_SIGNAL_API}/auth/logout`
|
? `${SIGNAL_API}/auth/logout`
|
||||||
: `${import.meta.env.VITE_CLOUD_API}/logout`;
|
: `${CLOUD_API}/logout`;
|
||||||
const res = await api.POST(logoutUrl);
|
const res = await api.POST(logoutUrl);
|
||||||
if (!res.ok) return;
|
if (!res.ok) return;
|
||||||
|
|
||||||
|
|
|
@ -26,6 +26,7 @@ import { InputFieldWithLabel } from "./InputField";
|
||||||
import DebianIcon from "@/assets/debian-icon.png";
|
import DebianIcon from "@/assets/debian-icon.png";
|
||||||
import UbuntuIcon from "@/assets/ubuntu-icon.png";
|
import UbuntuIcon from "@/assets/ubuntu-icon.png";
|
||||||
import FedoraIcon from "@/assets/fedora-icon.png";
|
import FedoraIcon from "@/assets/fedora-icon.png";
|
||||||
|
import OpenSUSEIcon from "@/assets/opensuse-icon.png";
|
||||||
import ArchIcon from "@/assets/arch-icon.png";
|
import ArchIcon from "@/assets/arch-icon.png";
|
||||||
import NetBootIcon from "@/assets/netboot-icon.svg";
|
import NetBootIcon from "@/assets/netboot-icon.svg";
|
||||||
import { TrashIcon } from "@heroicons/react/16/solid";
|
import { TrashIcon } from "@heroicons/react/16/solid";
|
||||||
|
@ -34,6 +35,7 @@ import { ExclamationTriangleIcon } from "@heroicons/react/20/solid";
|
||||||
import notifications from "../notifications";
|
import notifications from "../notifications";
|
||||||
import Fieldset from "./Fieldset";
|
import Fieldset from "./Fieldset";
|
||||||
import { isOnDevice } from "../main";
|
import { isOnDevice } from "../main";
|
||||||
|
import { SIGNAL_API } from "@/ui.config";
|
||||||
|
|
||||||
export default function MountMediaModal({
|
export default function MountMediaModal({
|
||||||
open,
|
open,
|
||||||
|
@ -542,6 +544,16 @@ function UrlView({
|
||||||
url: "https://download.fedoraproject.org/pub/fedora/linux/releases/41/Workstation/x86_64/iso/Fedora-Workstation-Live-x86_64-41-1.4.iso",
|
url: "https://download.fedoraproject.org/pub/fedora/linux/releases/41/Workstation/x86_64/iso/Fedora-Workstation-Live-x86_64-41-1.4.iso",
|
||||||
icon: FedoraIcon,
|
icon: FedoraIcon,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "openSUSE Leap 15.6",
|
||||||
|
url: "https://download.opensuse.org/distribution/leap/15.6/iso/openSUSE-Leap-15.6-NET-x86_64-Media.iso",
|
||||||
|
icon: OpenSUSEIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "openSUSE Tumbleweed",
|
||||||
|
url: "https://download.opensuse.org/tumbleweed/iso/openSUSE-Tumbleweed-NET-x86_64-Current.iso",
|
||||||
|
icon: OpenSUSEIcon,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "Arch Linux",
|
name: "Arch Linux",
|
||||||
url: "https://archlinux.doridian.net/iso/2025.02.01/archlinux-2025.02.01-x86_64.iso",
|
url: "https://archlinux.doridian.net/iso/2025.02.01/archlinux-2025.02.01-x86_64.iso",
|
||||||
|
@ -1108,7 +1120,7 @@ function UploadFileView({
|
||||||
alreadyUploadedBytes: number,
|
alreadyUploadedBytes: number,
|
||||||
dataChannel: string,
|
dataChannel: string,
|
||||||
) {
|
) {
|
||||||
const uploadUrl = `${import.meta.env.VITE_SIGNAL_API}/storage/upload?uploadId=${dataChannel}`;
|
const uploadUrl = `${SIGNAL_API}/storage/upload?uploadId=${dataChannel}`;
|
||||||
|
|
||||||
const xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
xhr.open("POST", uploadUrl, true);
|
xhr.open("POST", uploadUrl, true);
|
||||||
|
|
|
@ -1,32 +1,172 @@
|
||||||
import "react-simple-keyboard/build/css/index.css";
|
import "react-simple-keyboard/build/css/index.css";
|
||||||
import { useUiStore, useRTCStore } from "@/hooks/stores";
|
import { AvailableTerminalTypes, useUiStore } from "@/hooks/stores";
|
||||||
import { XTerm } from "./Xterm";
|
|
||||||
import { Button } from "./Button";
|
import { Button } from "./Button";
|
||||||
import { ChevronDownIcon } from "@heroicons/react/16/solid";
|
import { ChevronDownIcon } from "@heroicons/react/16/solid";
|
||||||
import { cx } from "../cva.config";
|
import { cx } from "@/cva.config";
|
||||||
import { Transition } from "@headlessui/react";
|
import { useEffect } from "react";
|
||||||
|
import { useXTerm } from "react-xtermjs";
|
||||||
|
import { FitAddon } from "@xterm/addon-fit";
|
||||||
|
import { WebLinksAddon } from "@xterm/addon-web-links";
|
||||||
|
import { WebglAddon } from "@xterm/addon-webgl";
|
||||||
|
import { Unicode11Addon } from "@xterm/addon-unicode11";
|
||||||
|
import { ClipboardAddon } from "@xterm/addon-clipboard";
|
||||||
|
|
||||||
function TerminalWrapper() {
|
const isWebGl2Supported = !!document.createElement("canvas").getContext("webgl2");
|
||||||
const enableTerminal = useUiStore(state => state.enableTerminal);
|
|
||||||
const setEnableTerminal = useUiStore(state => state.setEnableTerminal);
|
// Terminal theme configuration
|
||||||
const terminalChannel = useRTCStore(state => state.terminalChannel);
|
const SOLARIZED_THEME = {
|
||||||
|
background: "#0f172a", // Solarized base03
|
||||||
|
foreground: "#839496", // Solarized base0
|
||||||
|
cursor: "#93a1a1", // Solarized base1
|
||||||
|
cursorAccent: "#002b36", // Solarized base03
|
||||||
|
black: "#073642", // Solarized base02
|
||||||
|
red: "#dc322f", // Solarized red
|
||||||
|
green: "#859900", // Solarized green
|
||||||
|
yellow: "#b58900", // Solarized yellow
|
||||||
|
blue: "#268bd2", // Solarized blue
|
||||||
|
magenta: "#d33682", // Solarized magenta
|
||||||
|
cyan: "#2aa198", // Solarized cyan
|
||||||
|
white: "#eee8d5", // Solarized base2
|
||||||
|
brightBlack: "#002b36", // Solarized base03
|
||||||
|
brightRed: "#cb4b16", // Solarized orange
|
||||||
|
brightGreen: "#586e75", // Solarized base01
|
||||||
|
brightYellow: "#657b83", // Solarized base00
|
||||||
|
brightBlue: "#839496", // Solarized base0
|
||||||
|
brightMagenta: "#6c71c4", // Solarized violet
|
||||||
|
brightCyan: "#93a1a1", // Solarized base1
|
||||||
|
brightWhite: "#fdf6e3", // Solarized base3
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const TERMINAL_CONFIG = {
|
||||||
|
theme: SOLARIZED_THEME,
|
||||||
|
fontFamily: "'Fira Code', Menlo, Monaco, 'Courier New', monospace",
|
||||||
|
fontSize: 13,
|
||||||
|
allowProposedApi: true,
|
||||||
|
scrollback: 1000,
|
||||||
|
cursorBlink: true,
|
||||||
|
smoothScrollDuration: 100,
|
||||||
|
macOptionIsMeta: true,
|
||||||
|
macOptionClickForcesSelection: true,
|
||||||
|
convertEol: true,
|
||||||
|
linuxMode: false,
|
||||||
|
// Add these configurations:
|
||||||
|
cursorStyle: "block",
|
||||||
|
rendererType: "canvas", // Ensure we're using the canvas renderer
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
function Terminal({
|
||||||
|
title,
|
||||||
|
dataChannel,
|
||||||
|
type,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
dataChannel: RTCDataChannel;
|
||||||
|
type: AvailableTerminalTypes;
|
||||||
|
}) {
|
||||||
|
const enableTerminal = useUiStore(state => state.terminalType == type);
|
||||||
|
const setTerminalType = useUiStore(state => state.setTerminalType);
|
||||||
|
const setDisableKeyboardFocusTrap = useUiStore(state => state.setDisableVideoFocusTrap);
|
||||||
|
|
||||||
|
const { instance, ref } = useXTerm({ options: TERMINAL_CONFIG });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
setDisableKeyboardFocusTrap(enableTerminal);
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
setDisableKeyboardFocusTrap(false);
|
||||||
|
};
|
||||||
|
}, [enableTerminal, instance, ref, setDisableKeyboardFocusTrap, type]);
|
||||||
|
|
||||||
|
const readyState = dataChannel.readyState;
|
||||||
|
useEffect(() => {
|
||||||
|
if (readyState !== "open") return;
|
||||||
|
|
||||||
|
const abortController = new AbortController();
|
||||||
|
dataChannel.addEventListener(
|
||||||
|
"message",
|
||||||
|
e => {
|
||||||
|
instance?.write(new Uint8Array(e.data));
|
||||||
|
},
|
||||||
|
{ signal: abortController.signal },
|
||||||
|
);
|
||||||
|
|
||||||
|
const onDataHandler = instance?.onData(data => {
|
||||||
|
dataChannel.send(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Setup escape key handler
|
||||||
|
const onKeyHandler = instance?.onKey(e => {
|
||||||
|
const { domEvent } = e;
|
||||||
|
if (domEvent.key === "Escape") {
|
||||||
|
setTerminalType("none");
|
||||||
|
setDisableKeyboardFocusTrap(false);
|
||||||
|
domEvent.preventDefault();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
abortController.abort();
|
||||||
|
onDataHandler?.dispose();
|
||||||
|
onKeyHandler?.dispose();
|
||||||
|
};
|
||||||
|
}, [dataChannel, instance, readyState, setDisableKeyboardFocusTrap, setTerminalType]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!instance) return;
|
||||||
|
|
||||||
|
// Load the fit addon
|
||||||
|
const fitAddon = new FitAddon();
|
||||||
|
instance?.loadAddon(fitAddon);
|
||||||
|
|
||||||
|
instance?.loadAddon(new ClipboardAddon());
|
||||||
|
instance?.loadAddon(new Unicode11Addon());
|
||||||
|
instance?.loadAddon(new WebLinksAddon());
|
||||||
|
instance.unicode.activeVersion = "11";
|
||||||
|
|
||||||
|
if (isWebGl2Supported) {
|
||||||
|
const webGl2Addon = new WebglAddon();
|
||||||
|
webGl2Addon.onContextLoss(() => webGl2Addon.dispose());
|
||||||
|
instance?.loadAddon(webGl2Addon);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleResize = () => fitAddon.fit();
|
||||||
|
|
||||||
|
// Handle resize event
|
||||||
|
window.addEventListener("resize", handleResize);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("resize", handleResize);
|
||||||
|
};
|
||||||
|
}, [ref, instance, dataChannel]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div onKeyDown={e => e.stopPropagation()} onKeyUp={e => e.stopPropagation()}>
|
<div
|
||||||
<Transition show={enableTerminal} appear>
|
onKeyDown={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
onKeyUp={e => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
<div
|
<div
|
||||||
className={cx([
|
className={cx(
|
||||||
// Base styles
|
[
|
||||||
"fixed bottom-0 w-full transform transition duration-500 ease-in-out",
|
// Base styles
|
||||||
"translate-y-[0px]",
|
"fixed bottom-0 w-full transform transition duration-500 ease-in-out",
|
||||||
"data-[enter]:translate-y-[500px]",
|
"translate-y-[0px]",
|
||||||
"data-[closed]:translate-y-[500px]",
|
],
|
||||||
])}
|
{
|
||||||
|
"pointer-events-none translate-y-[500px] opacity-100 transition duration-300":
|
||||||
|
!enableTerminal,
|
||||||
|
"pointer-events-auto translate-y-[0px] opacity-100 transition duration-300":
|
||||||
|
enableTerminal,
|
||||||
|
},
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<div className="h-[500px] w-full bg-[#0f172a]">
|
<div className="h-[500px] w-full bg-[#0f172a]">
|
||||||
<div className="flex items-center justify-center px-2 py-1 bg-white dark:bg-slate-800 border-y border-y-slate-800/30 dark:border-y-slate-300/20">
|
<div className="flex items-center justify-center border-y border-y-slate-800/30 bg-white px-2 py-1 dark:border-y-slate-300/20 dark:bg-slate-800">
|
||||||
<h2 className="select-none self-center font-sans text-[12px] text-slate-700 dark:text-slate-300">
|
<h2 className="select-none self-center font-sans text-[12px] text-slate-700 dark:text-slate-300">
|
||||||
Web Terminal
|
{title}
|
||||||
</h2>
|
</h2>
|
||||||
<div className="absolute right-2">
|
<div className="absolute right-2">
|
||||||
<Button
|
<Button
|
||||||
|
@ -34,18 +174,19 @@ function TerminalWrapper() {
|
||||||
theme="light"
|
theme="light"
|
||||||
text="Hide"
|
text="Hide"
|
||||||
LeadingIcon={ChevronDownIcon}
|
LeadingIcon={ChevronDownIcon}
|
||||||
onClick={() => setEnableTerminal(false)}
|
onClick={() => setTerminalType("none")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="h-[calc(100%-36px)] p-3">
|
<div className="h-[calc(100%-36px)] p-3">
|
||||||
<XTerm terminalChannel={terminalChannel} />
|
<div ref={ref} style={{ height: "100%", width: "100%" }} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default TerminalWrapper;
|
export default Terminal;
|
||||||
|
|
|
@ -30,6 +30,8 @@ export default function WebRTCVideo() {
|
||||||
const {
|
const {
|
||||||
setClientSize: setVideoClientSize,
|
setClientSize: setVideoClientSize,
|
||||||
setSize: setVideoSize,
|
setSize: setVideoSize,
|
||||||
|
width: videoWidth,
|
||||||
|
height: videoHeight,
|
||||||
clientWidth: videoClientWidth,
|
clientWidth: videoClientWidth,
|
||||||
clientHeight: videoClientHeight,
|
clientHeight: videoClientHeight,
|
||||||
} = useVideoStore();
|
} = useVideoStore();
|
||||||
|
@ -102,20 +104,43 @@ export default function WebRTCVideo() {
|
||||||
const mouseMoveHandler = useCallback(
|
const mouseMoveHandler = useCallback(
|
||||||
(e: MouseEvent) => {
|
(e: MouseEvent) => {
|
||||||
if (!videoClientWidth || !videoClientHeight) return;
|
if (!videoClientWidth || !videoClientHeight) return;
|
||||||
const { buttons } = e;
|
// Get the aspect ratios of the video element and the video stream
|
||||||
|
const videoElementAspectRatio = videoClientWidth / videoClientHeight;
|
||||||
|
const videoStreamAspectRatio = videoWidth / videoHeight;
|
||||||
|
|
||||||
// Clamp mouse position within the video boundaries
|
// Calculate the effective video display area
|
||||||
const currMouseX = Math.min(Math.max(1, e.offsetX), videoClientWidth);
|
let effectiveWidth = videoClientWidth;
|
||||||
const currMouseY = Math.min(Math.max(1, e.offsetY), videoClientHeight);
|
let effectiveHeight = videoClientHeight;
|
||||||
|
let offsetX = 0;
|
||||||
|
let offsetY = 0;
|
||||||
|
|
||||||
// Normalize mouse position to 0-32767 range (HID absolute coordinate system)
|
if (videoElementAspectRatio > videoStreamAspectRatio) {
|
||||||
const x = Math.round((currMouseX / videoClientWidth) * 32767);
|
// Pillarboxing: black bars on the left and right
|
||||||
const y = Math.round((currMouseY / videoClientHeight) * 32767);
|
effectiveWidth = videoClientHeight * videoStreamAspectRatio;
|
||||||
|
offsetX = (videoClientWidth - effectiveWidth) / 2;
|
||||||
|
} else if (videoElementAspectRatio < videoStreamAspectRatio) {
|
||||||
|
// Letterboxing: black bars on the top and bottom
|
||||||
|
effectiveHeight = videoClientWidth / videoStreamAspectRatio;
|
||||||
|
offsetY = (videoClientHeight - effectiveHeight) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clamp mouse position within the effective video boundaries
|
||||||
|
const clampedX = Math.min(Math.max(offsetX, e.offsetX), offsetX + effectiveWidth);
|
||||||
|
const clampedY = Math.min(Math.max(offsetY, e.offsetY), offsetY + effectiveHeight);
|
||||||
|
|
||||||
|
// Map clamped mouse position to the video stream's coordinate system
|
||||||
|
const relativeX = (clampedX - offsetX) / effectiveWidth;
|
||||||
|
const relativeY = (clampedY - offsetY) / effectiveHeight;
|
||||||
|
|
||||||
|
// Convert to HID absolute coordinate system (0-32767 range)
|
||||||
|
const x = Math.round(relativeX * 32767);
|
||||||
|
const y = Math.round(relativeY * 32767);
|
||||||
|
|
||||||
// Send mouse movement
|
// Send mouse movement
|
||||||
|
const { buttons } = e;
|
||||||
sendMouseMovement(x, y, buttons);
|
sendMouseMovement(x, y, buttons);
|
||||||
},
|
},
|
||||||
[sendMouseMovement, videoClientHeight, videoClientWidth],
|
[sendMouseMovement, videoClientHeight, videoClientWidth, videoWidth, videoHeight],
|
||||||
);
|
);
|
||||||
|
|
||||||
const mouseWheelHandler = useCallback(
|
const mouseWheelHandler = useCallback(
|
||||||
|
|
|
@ -1,201 +0,0 @@
|
||||||
import { useEffect, useLayoutEffect, useRef } from "react";
|
|
||||||
import { Terminal } from "xterm";
|
|
||||||
import { Unicode11Addon } from "@xterm/addon-unicode11";
|
|
||||||
import { WebglAddon } from "@xterm/addon-webgl";
|
|
||||||
import { WebLinksAddon } from "@xterm/addon-web-links";
|
|
||||||
import { FitAddon } from "@xterm/addon-fit";
|
|
||||||
import { ClipboardAddon } from "@xterm/addon-clipboard";
|
|
||||||
|
|
||||||
import "xterm/css/xterm.css";
|
|
||||||
import { useRTCStore, useUiStore } from "../hooks/stores";
|
|
||||||
|
|
||||||
const isWebGl2Supported = !!document.createElement("canvas").getContext("webgl2");
|
|
||||||
|
|
||||||
// Add this debounce function at the top of the file
|
|
||||||
function debounce(func: (...args: any[]) => void, wait: number) {
|
|
||||||
let timeout: number | null = null;
|
|
||||||
return (...args: any[]) => {
|
|
||||||
if (timeout) clearTimeout(timeout);
|
|
||||||
timeout = setTimeout(() => func(...args), wait);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Terminal theme configuration
|
|
||||||
const SOLARIZED_THEME = {
|
|
||||||
background: "#0f172a", // Solarized base03
|
|
||||||
foreground: "#839496", // Solarized base0
|
|
||||||
cursor: "#93a1a1", // Solarized base1
|
|
||||||
cursorAccent: "#002b36", // Solarized base03
|
|
||||||
black: "#073642", // Solarized base02
|
|
||||||
red: "#dc322f", // Solarized red
|
|
||||||
green: "#859900", // Solarized green
|
|
||||||
yellow: "#b58900", // Solarized yellow
|
|
||||||
blue: "#268bd2", // Solarized blue
|
|
||||||
magenta: "#d33682", // Solarized magenta
|
|
||||||
cyan: "#2aa198", // Solarized cyan
|
|
||||||
white: "#eee8d5", // Solarized base2
|
|
||||||
brightBlack: "#002b36", // Solarized base03
|
|
||||||
brightRed: "#cb4b16", // Solarized orange
|
|
||||||
brightGreen: "#586e75", // Solarized base01
|
|
||||||
brightYellow: "#657b83", // Solarized base00
|
|
||||||
brightBlue: "#839496", // Solarized base0
|
|
||||||
brightMagenta: "#6c71c4", // Solarized violet
|
|
||||||
brightCyan: "#93a1a1", // Solarized base1
|
|
||||||
brightWhite: "#fdf6e3", // Solarized base3
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
const TERMINAL_CONFIG = {
|
|
||||||
theme: SOLARIZED_THEME,
|
|
||||||
fontFamily: "'Fira Code', Menlo, Monaco, 'Courier New', monospace",
|
|
||||||
fontSize: 13,
|
|
||||||
allowProposedApi: true,
|
|
||||||
scrollback: 1000,
|
|
||||||
cursorBlink: true,
|
|
||||||
smoothScrollDuration: 100,
|
|
||||||
macOptionIsMeta: true,
|
|
||||||
macOptionClickForcesSelection: true,
|
|
||||||
// Add these configurations:
|
|
||||||
convertEol: true,
|
|
||||||
linuxMode: false, // Disable Linux mode which might affect line endings
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
interface XTermProps {
|
|
||||||
terminalChannel: RTCDataChannel | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function XTerm({ terminalChannel }: XTermProps) {
|
|
||||||
const xtermRef = useRef<Terminal | null>(null);
|
|
||||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
||||||
const terminalElmRef = useRef<HTMLDivElement | null>(null);
|
|
||||||
const fitAddonRef = useRef<FitAddon | null>(null);
|
|
||||||
const setEnableTerminal = useUiStore(state => state.setEnableTerminal);
|
|
||||||
const setDisableKeyboardFocusTrap = useUiStore(state => state.setDisableVideoFocusTrap);
|
|
||||||
const peerConnection = useRTCStore(state => state.peerConnection);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setDisableKeyboardFocusTrap(true);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
setDisableKeyboardFocusTrap(false);
|
|
||||||
};
|
|
||||||
}, [setDisableKeyboardFocusTrap]);
|
|
||||||
|
|
||||||
const initializeTerminalAddons = (term: Terminal) => {
|
|
||||||
const fitAddon = new FitAddon();
|
|
||||||
term.loadAddon(fitAddon);
|
|
||||||
term.loadAddon(new ClipboardAddon());
|
|
||||||
term.loadAddon(new Unicode11Addon());
|
|
||||||
term.loadAddon(new WebLinksAddon());
|
|
||||||
term.unicode.activeVersion = "11";
|
|
||||||
|
|
||||||
if (isWebGl2Supported) {
|
|
||||||
const webGl2Addon = new WebglAddon();
|
|
||||||
webGl2Addon.onContextLoss(() => webGl2Addon.dispose());
|
|
||||||
term.loadAddon(webGl2Addon);
|
|
||||||
}
|
|
||||||
|
|
||||||
return fitAddon;
|
|
||||||
};
|
|
||||||
|
|
||||||
const setupTerminalChannel = (
|
|
||||||
term: Terminal,
|
|
||||||
channel: RTCDataChannel,
|
|
||||||
abortController: AbortController,
|
|
||||||
) => {
|
|
||||||
channel.onopen = () => {
|
|
||||||
// Handle terminal input
|
|
||||||
term.onData(data => {
|
|
||||||
if (channel.readyState === "open") {
|
|
||||||
channel.send(data);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle terminal output
|
|
||||||
channel.addEventListener(
|
|
||||||
"message",
|
|
||||||
(event: MessageEvent) => {
|
|
||||||
term.write(new Uint8Array(event.data));
|
|
||||||
},
|
|
||||||
{ signal: abortController.signal },
|
|
||||||
);
|
|
||||||
|
|
||||||
// Send initial terminal size
|
|
||||||
if (channel.readyState === "open") {
|
|
||||||
channel.send(JSON.stringify({ rows: term.rows, cols: term.cols }));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
if (!terminalElmRef.current) return;
|
|
||||||
|
|
||||||
// Ensure the container has dimensions before initializing
|
|
||||||
if (!terminalElmRef.current.offsetHeight || !terminalElmRef.current.offsetWidth) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const term = new Terminal(TERMINAL_CONFIG);
|
|
||||||
const fitAddon = initializeTerminalAddons(term);
|
|
||||||
const abortController = new AbortController();
|
|
||||||
|
|
||||||
// Setup escape key handler
|
|
||||||
term.onKey(e => {
|
|
||||||
const { domEvent } = e;
|
|
||||||
if (domEvent.key === "Escape") {
|
|
||||||
setEnableTerminal(false);
|
|
||||||
setDisableKeyboardFocusTrap(false);
|
|
||||||
domEvent.preventDefault();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let elm: HTMLDivElement | null = terminalElmRef.current;
|
|
||||||
// Initialize terminal
|
|
||||||
setTimeout(() => {
|
|
||||||
if (elm) {
|
|
||||||
console.log("opening terminal");
|
|
||||||
term.open(elm);
|
|
||||||
fitAddon.fit();
|
|
||||||
}
|
|
||||||
}, 800);
|
|
||||||
|
|
||||||
xtermRef.current = term;
|
|
||||||
fitAddonRef.current = fitAddon;
|
|
||||||
|
|
||||||
// Setup resize handling
|
|
||||||
const debouncedResizeHandler = debounce(() => fitAddon.fit(), 100);
|
|
||||||
const resizeObserver = new ResizeObserver(debouncedResizeHandler);
|
|
||||||
resizeObserver.observe(terminalElmRef.current);
|
|
||||||
|
|
||||||
// Focus terminal after a short delay
|
|
||||||
setTimeout(() => {
|
|
||||||
term.focus();
|
|
||||||
terminalElmRef.current?.focus();
|
|
||||||
}, 500);
|
|
||||||
|
|
||||||
// Setup terminal channel if available
|
|
||||||
const channel = peerConnection?.createDataChannel("terminal");
|
|
||||||
if (channel) {
|
|
||||||
setupTerminalChannel(term, channel, abortController);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cleanup
|
|
||||||
return () => {
|
|
||||||
resizeObserver.disconnect();
|
|
||||||
abortController.abort();
|
|
||||||
term.dispose();
|
|
||||||
elm = null;
|
|
||||||
xtermRef.current = null;
|
|
||||||
fitAddonRef.current = null;
|
|
||||||
};
|
|
||||||
}, [peerConnection, setDisableKeyboardFocusTrap, setEnableTerminal, terminalChannel]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="w-full h-full" ref={containerRef}>
|
|
||||||
<div
|
|
||||||
className="w-full h-full terminal-container"
|
|
||||||
ref={terminalElmRef}
|
|
||||||
style={{ display: "flex", minHeight: "100%" }}
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -0,0 +1,171 @@
|
||||||
|
import { Button } from "@components/Button";
|
||||||
|
import { LuHardDrive, LuPower, LuRotateCcw } from "react-icons/lu";
|
||||||
|
import Card from "@components/Card";
|
||||||
|
import { SectionHeader } from "@components/SectionHeader";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import notifications from "@/notifications";
|
||||||
|
import { useJsonRpc } from "../../hooks/useJsonRpc";
|
||||||
|
import LoadingSpinner from "../LoadingSpinner";
|
||||||
|
|
||||||
|
const LONG_PRESS_DURATION = 3000; // 3 seconds for long press
|
||||||
|
|
||||||
|
interface ATXState {
|
||||||
|
power: boolean;
|
||||||
|
hdd: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ATXPowerControl() {
|
||||||
|
const [isPowerPressed, setIsPowerPressed] = useState(false);
|
||||||
|
const [powerPressTimer, setPowerPressTimer] = useState<ReturnType<
|
||||||
|
typeof setTimeout
|
||||||
|
> | null>(null);
|
||||||
|
const [atxState, setAtxState] = useState<ATXState | null>(null);
|
||||||
|
|
||||||
|
const [send] = useJsonRpc(function onRequest(resp) {
|
||||||
|
if (resp.method === "atxState") {
|
||||||
|
setAtxState(resp.params as ATXState);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Request initial state
|
||||||
|
useEffect(() => {
|
||||||
|
send("getATXState", {}, resp => {
|
||||||
|
if ("error" in resp) {
|
||||||
|
notifications.error(
|
||||||
|
`Failed to get ATX state: ${resp.error.data || "Unknown error"}`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setAtxState(resp.result as ATXState);
|
||||||
|
});
|
||||||
|
}, [send]);
|
||||||
|
|
||||||
|
const handlePowerPress = (pressed: boolean) => {
|
||||||
|
// Prevent phantom releases
|
||||||
|
if (!pressed && !isPowerPressed) return;
|
||||||
|
|
||||||
|
setIsPowerPressed(pressed);
|
||||||
|
|
||||||
|
// Handle button press
|
||||||
|
if (pressed) {
|
||||||
|
// Start long press timer
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
// Send long press action
|
||||||
|
console.log("Sending long press ATX power action");
|
||||||
|
send("setATXPowerAction", { action: "power-long" }, resp => {
|
||||||
|
if ("error" in resp) {
|
||||||
|
notifications.error(
|
||||||
|
`Failed to send ATX power action: ${resp.error.data || "Unknown error"}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
setIsPowerPressed(false);
|
||||||
|
});
|
||||||
|
}, LONG_PRESS_DURATION);
|
||||||
|
|
||||||
|
setPowerPressTimer(timer);
|
||||||
|
}
|
||||||
|
// Handle button release
|
||||||
|
else {
|
||||||
|
// If timer exists, was a short press
|
||||||
|
if (powerPressTimer) {
|
||||||
|
clearTimeout(powerPressTimer);
|
||||||
|
setPowerPressTimer(null);
|
||||||
|
|
||||||
|
// Send short press action
|
||||||
|
console.log("Sending short press ATX power action");
|
||||||
|
send("setATXPowerAction", { action: "power-short" }, resp => {
|
||||||
|
if ("error" in resp) {
|
||||||
|
notifications.error(
|
||||||
|
`Failed to send ATX power action: ${resp.error.data || "Unknown error"}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Cleanup timer on unmount
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (powerPressTimer) {
|
||||||
|
clearTimeout(powerPressTimer);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [powerPressTimer]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<SectionHeader
|
||||||
|
title="ATX Power Control"
|
||||||
|
description="Control your ATX power settings"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{atxState === null ? (
|
||||||
|
<Card className="flex h-[120px] items-center justify-center p-3">
|
||||||
|
<LoadingSpinner className="w-6 h-6 text-blue-500 dark:text-blue-400" />
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
<Card className="h-[120px] animate-fadeIn opacity-0">
|
||||||
|
<div className="p-3 space-y-4">
|
||||||
|
{/* Control Buttons */}
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Button
|
||||||
|
size="SM"
|
||||||
|
theme="light"
|
||||||
|
LeadingIcon={LuPower}
|
||||||
|
text="Power"
|
||||||
|
onMouseDown={() => handlePowerPress(true)}
|
||||||
|
onMouseUp={() => handlePowerPress(false)}
|
||||||
|
onMouseLeave={() => handlePowerPress(false)}
|
||||||
|
className={isPowerPressed ? "opacity-75" : ""}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size="SM"
|
||||||
|
theme="light"
|
||||||
|
LeadingIcon={LuRotateCcw}
|
||||||
|
text="Reset"
|
||||||
|
onClick={() => {
|
||||||
|
send("setATXPowerAction", { action: "reset" }, resp => {
|
||||||
|
if ("error" in resp) {
|
||||||
|
notifications.error(
|
||||||
|
`Failed to send ATX power action: ${resp.error.data || "Unknown error"}`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr className="border-slate-700/30 dark:border-slate-600/30" />
|
||||||
|
{/* Status Indicators */}
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||||
|
<LuPower
|
||||||
|
strokeWidth={3}
|
||||||
|
className={`mr-1 inline ${
|
||||||
|
atxState?.power ? "text-green-600" : "text-slate-300"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
Power LED
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||||
|
<LuHardDrive
|
||||||
|
strokeWidth={3}
|
||||||
|
className={`mr-1 inline ${
|
||||||
|
atxState?.hdd ? "text-blue-400" : "text-slate-300"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
HDD LED
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,114 @@
|
||||||
|
import { Button } from "@components/Button";
|
||||||
|
import { LuPower } from "react-icons/lu";
|
||||||
|
import Card from "@components/Card";
|
||||||
|
import { SectionHeader } from "@components/SectionHeader";
|
||||||
|
import FieldLabel from "../FieldLabel";
|
||||||
|
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import notifications from "@/notifications";
|
||||||
|
import LoadingSpinner from "../LoadingSpinner";
|
||||||
|
|
||||||
|
interface DCPowerState {
|
||||||
|
isOn: boolean;
|
||||||
|
voltage: number;
|
||||||
|
current: number;
|
||||||
|
power: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DCPowerControl() {
|
||||||
|
const [send] = useJsonRpc();
|
||||||
|
const [powerState, setPowerState] = useState<DCPowerState | null>(null);
|
||||||
|
|
||||||
|
const getDCPowerState = useCallback(() => {
|
||||||
|
send("getDCPowerState", {}, resp => {
|
||||||
|
if ("error" in resp) {
|
||||||
|
notifications.error(
|
||||||
|
`Failed to get DC power state: ${resp.error.data || "Unknown error"}`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setPowerState(resp.result as DCPowerState);
|
||||||
|
});
|
||||||
|
}, [send]);
|
||||||
|
|
||||||
|
const handlePowerToggle = (enabled: boolean) => {
|
||||||
|
send("setDCPowerState", { enabled }, resp => {
|
||||||
|
if ("error" in resp) {
|
||||||
|
notifications.error(
|
||||||
|
`Failed to set DC power state: ${resp.error.data || "Unknown error"}`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
getDCPowerState(); // Refresh state after change
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getDCPowerState();
|
||||||
|
// Set up polling interval to update status
|
||||||
|
const interval = setInterval(getDCPowerState, 1000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [getDCPowerState]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<SectionHeader
|
||||||
|
title="DC Power Control"
|
||||||
|
description="Control your DC power settings"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{powerState === null ? (
|
||||||
|
<Card className="flex h-[160px] justify-center p-3">
|
||||||
|
<LoadingSpinner className="w-6 h-6 text-blue-500 dark:text-blue-400" />
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
<Card className="h-[160px] animate-fadeIn opacity-0">
|
||||||
|
<div className="p-3 space-y-4">
|
||||||
|
{/* Power Controls */}
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Button
|
||||||
|
size="SM"
|
||||||
|
theme="light"
|
||||||
|
LeadingIcon={LuPower}
|
||||||
|
text="Power On"
|
||||||
|
onClick={() => handlePowerToggle(true)}
|
||||||
|
disabled={powerState.isOn}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size="SM"
|
||||||
|
theme="light"
|
||||||
|
LeadingIcon={LuPower}
|
||||||
|
text="Power Off"
|
||||||
|
disabled={!powerState.isOn}
|
||||||
|
onClick={() => handlePowerToggle(false)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<hr className="border-slate-700/30 dark:border-slate-600/30" />
|
||||||
|
|
||||||
|
{/* Status Display */}
|
||||||
|
<div className="grid grid-cols-3 gap-4">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<FieldLabel label="Voltage" />
|
||||||
|
<p className="text-sm font-medium text-slate-900 dark:text-slate-100">
|
||||||
|
{powerState.voltage.toFixed(1)}V
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<FieldLabel label="Current" />
|
||||||
|
<p className="text-sm font-medium text-slate-900 dark:text-slate-100">
|
||||||
|
{powerState.current.toFixed(1)}A
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<FieldLabel label="Power" />
|
||||||
|
<p className="text-sm font-medium text-slate-900 dark:text-slate-100">
|
||||||
|
{powerState.power.toFixed(1)}W
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,130 @@
|
||||||
|
import { Button } from "@components/Button";
|
||||||
|
import { LuTerminal } from "react-icons/lu";
|
||||||
|
import Card from "@components/Card";
|
||||||
|
import { SectionHeader } from "@components/SectionHeader";
|
||||||
|
import { SelectMenuBasic } from "../SelectMenuBasic";
|
||||||
|
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import notifications from "@/notifications";
|
||||||
|
import { useUiStore } from "@/hooks/stores";
|
||||||
|
|
||||||
|
interface SerialSettings {
|
||||||
|
baudRate: string;
|
||||||
|
dataBits: string;
|
||||||
|
stopBits: string;
|
||||||
|
parity: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SerialConsole() {
|
||||||
|
const [send] = useJsonRpc();
|
||||||
|
const [settings, setSettings] = useState<SerialSettings>({
|
||||||
|
baudRate: "9600",
|
||||||
|
dataBits: "8",
|
||||||
|
stopBits: "1",
|
||||||
|
parity: "none",
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
send("getSerialSettings", {}, resp => {
|
||||||
|
if ("error" in resp) {
|
||||||
|
notifications.error(
|
||||||
|
`Failed to get serial settings: ${resp.error.data || "Unknown error"}`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSettings(resp.result as SerialSettings);
|
||||||
|
});
|
||||||
|
}, [send]);
|
||||||
|
|
||||||
|
const handleSettingChange = (setting: keyof SerialSettings, value: string) => {
|
||||||
|
const newSettings = { ...settings, [setting]: value };
|
||||||
|
send("setSerialSettings", { settings: newSettings }, resp => {
|
||||||
|
if ("error" in resp) {
|
||||||
|
notifications.error(
|
||||||
|
`Failed to update serial settings: ${resp.error.data || "Unknown error"}`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSettings(newSettings);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const setTerminalType = useUiStore(state => state.setTerminalType);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<SectionHeader
|
||||||
|
title="Serial Console"
|
||||||
|
description="Configure your serial console settings"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Card className="animate-fadeIn opacity-0">
|
||||||
|
<div className="space-y-4 p-3">
|
||||||
|
{/* Open Console Button */}
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Button
|
||||||
|
size="SM"
|
||||||
|
theme="primary"
|
||||||
|
LeadingIcon={LuTerminal}
|
||||||
|
text="Open Console"
|
||||||
|
onClick={() => {
|
||||||
|
setTerminalType("serial");
|
||||||
|
console.log("Opening serial console with settings: ", settings);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<hr className="border-slate-700/30 dark:border-slate-600/30" />
|
||||||
|
{/* Settings */}
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<SelectMenuBasic
|
||||||
|
label="Baud Rate"
|
||||||
|
options={[
|
||||||
|
{ label: "1200", value: "1200" },
|
||||||
|
{ label: "2400", value: "2400" },
|
||||||
|
{ label: "4800", value: "4800" },
|
||||||
|
{ label: "9600", value: "9600" },
|
||||||
|
{ label: "19200", value: "19200" },
|
||||||
|
{ label: "38400", value: "38400" },
|
||||||
|
{ label: "57600", value: "57600" },
|
||||||
|
{ label: "115200", value: "115200" },
|
||||||
|
]}
|
||||||
|
value={settings.baudRate}
|
||||||
|
onChange={e => handleSettingChange("baudRate", e.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectMenuBasic
|
||||||
|
label="Data Bits"
|
||||||
|
options={[
|
||||||
|
{ label: "8", value: "8" },
|
||||||
|
{ label: "7", value: "7" },
|
||||||
|
]}
|
||||||
|
value={settings.dataBits}
|
||||||
|
onChange={e => handleSettingChange("dataBits", e.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectMenuBasic
|
||||||
|
label="Stop Bits"
|
||||||
|
options={[
|
||||||
|
{ label: "1", value: "1" },
|
||||||
|
{ label: "1.5", value: "1.5" },
|
||||||
|
{ label: "2", value: "2" },
|
||||||
|
]}
|
||||||
|
value={settings.stopBits}
|
||||||
|
onChange={e => handleSettingChange("stopBits", e.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectMenuBasic
|
||||||
|
label="Parity"
|
||||||
|
options={[
|
||||||
|
{ label: "None", value: "none" },
|
||||||
|
{ label: "Even", value: "even" },
|
||||||
|
{ label: "Odd", value: "odd" },
|
||||||
|
]}
|
||||||
|
value={settings.parity}
|
||||||
|
onChange={e => handleSettingChange("parity", e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,145 @@
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||||
|
import Card, { GridCard } from "@components/Card";
|
||||||
|
import { SectionHeader } from "@components/SectionHeader";
|
||||||
|
import { Button } from "../Button";
|
||||||
|
import { LuPower, LuTerminal, LuPlugZap } from "react-icons/lu";
|
||||||
|
import { ATXPowerControl } from "@components/extensions/ATXPowerControl";
|
||||||
|
import { DCPowerControl } from "@components/extensions/DCPowerControl";
|
||||||
|
import { SerialConsole } from "@components/extensions/SerialConsole";
|
||||||
|
import notifications from "../../notifications";
|
||||||
|
|
||||||
|
interface Extension {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
icon: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AVAILABLE_EXTENSIONS: Extension[] = [
|
||||||
|
{
|
||||||
|
id: "atx-power",
|
||||||
|
name: "ATX Power Control",
|
||||||
|
description: "Control your ATX Power extension",
|
||||||
|
icon: LuPower,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "dc-power",
|
||||||
|
name: "DC Power Control",
|
||||||
|
description: "Control your DC Power extension",
|
||||||
|
icon: LuPlugZap,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "serial-console",
|
||||||
|
name: "Serial Console",
|
||||||
|
description: "Access your serial console extension",
|
||||||
|
icon: LuTerminal,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function ExtensionPopover() {
|
||||||
|
const [send] = useJsonRpc();
|
||||||
|
const [activeExtension, setActiveExtension] = useState<Extension | null>(null);
|
||||||
|
|
||||||
|
// Load active extension on component mount
|
||||||
|
useEffect(() => {
|
||||||
|
send("getActiveExtension", {}, resp => {
|
||||||
|
if ("error" in resp) return;
|
||||||
|
const extensionId = resp.result as string;
|
||||||
|
if (extensionId) {
|
||||||
|
const extension = AVAILABLE_EXTENSIONS.find(ext => ext.id === extensionId);
|
||||||
|
if (extension) {
|
||||||
|
setActiveExtension(extension);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [send]);
|
||||||
|
|
||||||
|
const handleSetActiveExtension = (extension: Extension | null) => {
|
||||||
|
send("setActiveExtension", { extensionId: extension?.id || "" }, resp => {
|
||||||
|
if ("error" in resp) {
|
||||||
|
notifications.error(`Failed to set active extension: ${resp.error.data || "Unknown error"}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setActiveExtension(extension);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderActiveExtension = () => {
|
||||||
|
switch (activeExtension?.id) {
|
||||||
|
case "atx-power":
|
||||||
|
return <ATXPowerControl />;
|
||||||
|
case "dc-power":
|
||||||
|
return <DCPowerControl />;
|
||||||
|
case "serial-console":
|
||||||
|
return <SerialConsole />;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GridCard>
|
||||||
|
<div className="p-4 py-3 space-y-4">
|
||||||
|
<div className="grid h-full grid-rows-headerBody">
|
||||||
|
<div className="space-y-4">
|
||||||
|
{activeExtension ? (
|
||||||
|
// Extension Control View
|
||||||
|
<div className="space-y-4">
|
||||||
|
{renderActiveExtension()}
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="flex items-center justify-end space-x-2 opacity-0 animate-fadeIn"
|
||||||
|
style={{
|
||||||
|
animationDuration: "0.7s",
|
||||||
|
animationDelay: "0.2s",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
size="SM"
|
||||||
|
theme="light"
|
||||||
|
text="Unload Extension"
|
||||||
|
onClick={() => handleSetActiveExtension(null)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
// Extensions List View
|
||||||
|
<div className="space-y-4">
|
||||||
|
<SectionHeader
|
||||||
|
title="Extensions"
|
||||||
|
description="Load and manage your extensions"
|
||||||
|
/>
|
||||||
|
<Card className="opacity-0 animate-fadeIn">
|
||||||
|
<div className="w-full divide-y divide-slate-700/30 dark:divide-slate-600/30">
|
||||||
|
{AVAILABLE_EXTENSIONS.map(extension => (
|
||||||
|
<div
|
||||||
|
key={extension.id}
|
||||||
|
className="flex items-center justify-between p-3"
|
||||||
|
>
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<p className="text-sm font-semibold leading-none text-slate-900 dark:text-slate-100">
|
||||||
|
{extension.name}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-slate-600 dark:text-slate-400">
|
||||||
|
{extension.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
size="XS"
|
||||||
|
theme="light"
|
||||||
|
text="Load"
|
||||||
|
onClick={() => handleSetActiveExtension(extension)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</GridCard>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
import SidebarHeader from "@components/SidebarHeader";
|
import SidebarHeader from "@components/SidebarHeader";
|
||||||
import {
|
import {
|
||||||
|
BacklightSettings,
|
||||||
useLocalAuthModalStore,
|
useLocalAuthModalStore,
|
||||||
useSettingsStore,
|
useSettingsStore,
|
||||||
useUiStore,
|
useUiStore,
|
||||||
|
@ -25,6 +26,7 @@ import LocalAuthPasswordDialog from "@/components/LocalAuthPasswordDialog";
|
||||||
import { LocalDevice } from "@routes/devices.$id";
|
import { LocalDevice } from "@routes/devices.$id";
|
||||||
import { useRevalidator } from "react-router-dom";
|
import { useRevalidator } from "react-router-dom";
|
||||||
import { ShieldCheckIcon } from "@heroicons/react/20/solid";
|
import { ShieldCheckIcon } from "@heroicons/react/20/solid";
|
||||||
|
import { CLOUD_APP, SIGNAL_API } from "@/ui.config";
|
||||||
|
|
||||||
export function SettingsItem({
|
export function SettingsItem({
|
||||||
title,
|
title,
|
||||||
|
@ -95,6 +97,7 @@ export default function SettingsSidebar() {
|
||||||
const hideCursor = useSettingsStore(state => state.isCursorHidden);
|
const hideCursor = useSettingsStore(state => state.isCursorHidden);
|
||||||
const setHideCursor = useSettingsStore(state => state.setCursorVisibility);
|
const setHideCursor = useSettingsStore(state => state.setCursorVisibility);
|
||||||
const setDeveloperMode = useSettingsStore(state => state.setDeveloperMode);
|
const setDeveloperMode = useSettingsStore(state => state.setDeveloperMode);
|
||||||
|
const setBacklightSettings = useSettingsStore(state => state.setBacklightSettings);
|
||||||
|
|
||||||
const [currentVersions, setCurrentVersions] = useState<{
|
const [currentVersions, setCurrentVersions] = useState<{
|
||||||
appVersion: string;
|
appVersion: string;
|
||||||
|
@ -228,6 +231,28 @@ export default function SettingsSidebar() {
|
||||||
[send, setDeveloperMode],
|
[send, setDeveloperMode],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleBacklightSettingsSave = () => {
|
||||||
|
send("setBacklightSettings", { params: settings.backlightSettings }, resp => {
|
||||||
|
if ("error" in resp) {
|
||||||
|
notifications.error(
|
||||||
|
`Failed to set backlight settings: ${resp.error.data || "Unknown error"}`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
notifications.success("Backlight settings updated successfully");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const handleUpdateSSHKey = useCallback(() => {
|
const handleUpdateSSHKey = useCallback(() => {
|
||||||
send("setSSHKeyState", { sshKey }, resp => {
|
send("setSSHKeyState", { sshKey }, resp => {
|
||||||
if ("error" in resp) {
|
if ("error" in resp) {
|
||||||
|
@ -302,6 +327,17 @@ export default function SettingsSidebar() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
send("getBacklightSettings", {}, resp => {
|
||||||
|
if ("error" in resp) {
|
||||||
|
notifications.error(
|
||||||
|
`Failed to get backlight settings: ${resp.error.data || "Unknown error"}`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const result = resp.result as BacklightSettings;
|
||||||
|
setBacklightSettings(result);
|
||||||
|
})
|
||||||
|
|
||||||
send("getDevModeState", {}, resp => {
|
send("getDevModeState", {}, resp => {
|
||||||
if ("error" in resp) return;
|
if ("error" in resp) return;
|
||||||
const result = resp.result as { enabled: boolean };
|
const result = resp.result as { enabled: boolean };
|
||||||
|
@ -331,7 +367,7 @@ export default function SettingsSidebar() {
|
||||||
const getDevice = useCallback(async () => {
|
const getDevice = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const status = await api
|
const status = await api
|
||||||
.GET(`${import.meta.env.VITE_SIGNAL_API}/device`)
|
.GET(`${SIGNAL_API}/device`)
|
||||||
.then(res => res.json() as Promise<LocalDevice>);
|
.then(res => res.json() as Promise<LocalDevice>);
|
||||||
setLocalDevice(status);
|
setLocalDevice(status);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -466,7 +502,7 @@ export default function SettingsSidebar() {
|
||||||
<GridCard>
|
<GridCard>
|
||||||
<div className="flex items-center px-4 py-3 group gap-x-4">
|
<div className="flex items-center px-4 py-3 group gap-x-4">
|
||||||
<img
|
<img
|
||||||
className="w-6 shrink-0"
|
className="w-6 shrink-0 dark:invert"
|
||||||
src={PointingFinger}
|
src={PointingFinger}
|
||||||
alt="Finger touching a screen"
|
alt="Finger touching a screen"
|
||||||
/>
|
/>
|
||||||
|
@ -490,7 +526,7 @@ export default function SettingsSidebar() {
|
||||||
>
|
>
|
||||||
<GridCard>
|
<GridCard>
|
||||||
<div className="flex items-center px-4 py-3 gap-x-4">
|
<div className="flex items-center px-4 py-3 gap-x-4">
|
||||||
<img className="w-6 shrink-0" src={MouseIcon} alt="Mouse icon" />
|
<img className="w-6 shrink-0 dark:invert" src={MouseIcon} alt="Mouse icon" />
|
||||||
<div className="flex items-center justify-between grow">
|
<div className="flex items-center justify-between grow">
|
||||||
<div className="text-left">
|
<div className="text-left">
|
||||||
<h3 className="text-sm font-semibold text-black dark:text-white">
|
<h3 className="text-sm font-semibold text-black dark:text-white">
|
||||||
|
@ -642,7 +678,7 @@ export default function SettingsSidebar() {
|
||||||
<div>
|
<div>
|
||||||
<LinkButton
|
<LinkButton
|
||||||
to={
|
to={
|
||||||
import.meta.env.VITE_CLOUD_APP +
|
CLOUD_APP +
|
||||||
"/signup?deviceId=" +
|
"/signup?deviceId=" +
|
||||||
deviceId +
|
deviceId +
|
||||||
`&returnTo=${location.href}adopt`
|
`&returnTo=${location.href}adopt`
|
||||||
|
@ -797,6 +833,80 @@ export default function SettingsSidebar() {
|
||||||
/>
|
/>
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
<div className="h-[1px] w-full bg-slate-800/10 dark:bg-slate-300/20" />
|
<div className="h-[1px] w-full bg-slate-800/10 dark:bg-slate-300/20" />
|
||||||
|
<div className="pb-2 space-y-4">
|
||||||
|
<SectionHeader
|
||||||
|
title="Hardware"
|
||||||
|
description="Configure the JetKVM Hardware"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<SettingsItem title="Display Brightness" description="Set the brightness of the display">
|
||||||
|
<SelectMenuBasic
|
||||||
|
size="SM"
|
||||||
|
label=""
|
||||||
|
value={settings.backlightSettings.max_brightness.toString()}
|
||||||
|
options={[
|
||||||
|
{ value: "0", label: "Off" },
|
||||||
|
{ value: "10", label: "Low" },
|
||||||
|
{ value: "35", label: "Medium" },
|
||||||
|
{ value: "64", label: "High" },
|
||||||
|
]}
|
||||||
|
onChange={e => {
|
||||||
|
settings.backlightSettings.max_brightness = parseInt(e.target.value)
|
||||||
|
handleBacklightSettingsChange(settings.backlightSettings);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</SettingsItem>
|
||||||
|
{settings.backlightSettings.max_brightness != 0 && (
|
||||||
|
<>
|
||||||
|
<SettingsItem title="Dim Display After" description="Set how long to wait before dimming the display">
|
||||||
|
<SelectMenuBasic
|
||||||
|
size="SM"
|
||||||
|
label=""
|
||||||
|
value={settings.backlightSettings.dim_after.toString()}
|
||||||
|
options={[
|
||||||
|
{ value: "0", label: "Never" },
|
||||||
|
{ value: "60", label: "1 Minute" },
|
||||||
|
{ value: "300", label: "5 Minutes" },
|
||||||
|
{ value: "600", label: "10 Minutes" },
|
||||||
|
{ value: "1800", label: "30 Minutes" },
|
||||||
|
{ value: "3600", label: "1 Hour" },
|
||||||
|
]}
|
||||||
|
onChange={e => {
|
||||||
|
settings.backlightSettings.dim_after = parseInt(e.target.value)
|
||||||
|
handleBacklightSettingsChange(settings.backlightSettings);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</SettingsItem>
|
||||||
|
<SettingsItem title="Turn off Display After" description="Set how long to wait before turning off the display">
|
||||||
|
<SelectMenuBasic
|
||||||
|
size="SM"
|
||||||
|
label=""
|
||||||
|
value={settings.backlightSettings.off_after.toString()}
|
||||||
|
options={[
|
||||||
|
{ value: "0", label: "Never" },
|
||||||
|
{ value: "300", label: "5 Minutes" },
|
||||||
|
{ value: "600", label: "10 Minutes" },
|
||||||
|
{ value: "1800", label: "30 Minutes" },
|
||||||
|
{ value: "3600", label: "1 Hour" },
|
||||||
|
]}
|
||||||
|
onChange={e => {
|
||||||
|
settings.backlightSettings.off_after = parseInt(e.target.value)
|
||||||
|
handleBacklightSettingsChange(settings.backlightSettings);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</SettingsItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<p className="text-xs text-slate-600 dark:text-slate-400">
|
||||||
|
The display will wake up when the connection state changes, or when touched.
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
size="SM"
|
||||||
|
theme="primary"
|
||||||
|
text="Save Display Settings"
|
||||||
|
onClick={handleBacklightSettingsSave}
|
||||||
|
/>
|
||||||
|
<div className="h-[1px] w-full bg-slate-800/10 dark:bg-slate-300/20" />
|
||||||
<div className="pb-2 space-y-4">
|
<div className="pb-2 space-y-4">
|
||||||
<SectionHeader
|
<SectionHeader
|
||||||
title="Advanced"
|
title="Advanced"
|
||||||
|
|
|
@ -22,6 +22,7 @@ const appendStatToMap = <T extends { timestamp: number }>(
|
||||||
// Constants and types
|
// Constants and types
|
||||||
export type AvailableSidebarViews = "system" | "connection-stats";
|
export type AvailableSidebarViews = "system" | "connection-stats";
|
||||||
export type AvailableModalViews = "connection-stats" | "settings";
|
export type AvailableModalViews = "connection-stats" | "settings";
|
||||||
|
export type AvailableTerminalTypes = "kvm" | "serial" | "none";
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
sub: string;
|
sub: string;
|
||||||
|
@ -52,13 +53,13 @@ interface UIState {
|
||||||
isAttachedVirtualKeyboardVisible: boolean;
|
isAttachedVirtualKeyboardVisible: boolean;
|
||||||
setAttachedVirtualKeyboardVisibility: (enabled: boolean) => void;
|
setAttachedVirtualKeyboardVisibility: (enabled: boolean) => void;
|
||||||
|
|
||||||
enableTerminal: boolean;
|
terminalType: AvailableTerminalTypes;
|
||||||
setEnableTerminal: (enabled: UIState["enableTerminal"]) => void;
|
setTerminalType: (enabled: UIState["terminalType"]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useUiStore = create<UIState>(set => ({
|
export const useUiStore = create<UIState>(set => ({
|
||||||
enableTerminal: false,
|
terminalType: "none",
|
||||||
setEnableTerminal: enabled => set({ enableTerminal: enabled }),
|
setTerminalType: type => set({ terminalType: type }),
|
||||||
|
|
||||||
sidebarView: null,
|
sidebarView: null,
|
||||||
setSidebarView: view => set({ sidebarView: view }),
|
setSidebarView: view => set({ sidebarView: view }),
|
||||||
|
@ -229,6 +230,12 @@ export interface VideoState {
|
||||||
}) => void;
|
}) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface BacklightSettings {
|
||||||
|
max_brightness: number;
|
||||||
|
dim_after: number;
|
||||||
|
off_after: number;
|
||||||
|
}
|
||||||
|
|
||||||
export const useVideoStore = create<VideoState>(set => ({
|
export const useVideoStore = create<VideoState>(set => ({
|
||||||
width: 0,
|
width: 0,
|
||||||
height: 0,
|
height: 0,
|
||||||
|
@ -270,6 +277,9 @@ interface SettingsState {
|
||||||
// Add new developer mode state
|
// Add new developer mode state
|
||||||
developerMode: boolean;
|
developerMode: boolean;
|
||||||
setDeveloperMode: (enabled: boolean) => void;
|
setDeveloperMode: (enabled: boolean) => void;
|
||||||
|
|
||||||
|
backlightSettings: BacklightSettings;
|
||||||
|
setBacklightSettings: (settings: BacklightSettings) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useSettingsStore = create(
|
export const useSettingsStore = create(
|
||||||
|
@ -287,6 +297,13 @@ export const useSettingsStore = create(
|
||||||
// Add developer mode with default value
|
// Add developer mode with default value
|
||||||
developerMode: false,
|
developerMode: false,
|
||||||
setDeveloperMode: enabled => set({ developerMode: enabled }),
|
setDeveloperMode: enabled => set({ developerMode: enabled }),
|
||||||
|
|
||||||
|
backlightSettings: {
|
||||||
|
max_brightness: 100,
|
||||||
|
dim_after: 10000,
|
||||||
|
off_after: 50000,
|
||||||
|
},
|
||||||
|
setBacklightSettings: (settings: BacklightSettings) => set({ backlightSettings: settings }),
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: "settings",
|
name: "settings",
|
||||||
|
|
|
@ -27,12 +27,13 @@ import LoginLocalRoute from "./routes/login-local";
|
||||||
import WelcomeLocalModeRoute from "./routes/welcome-local.mode";
|
import WelcomeLocalModeRoute from "./routes/welcome-local.mode";
|
||||||
import WelcomeRoute from "./routes/welcome-local";
|
import WelcomeRoute from "./routes/welcome-local";
|
||||||
import WelcomeLocalPasswordRoute from "./routes/welcome-local.password";
|
import WelcomeLocalPasswordRoute from "./routes/welcome-local.password";
|
||||||
|
import { CLOUD_API } from "./ui.config";
|
||||||
|
|
||||||
export const isOnDevice = import.meta.env.MODE === "device";
|
export const isOnDevice = import.meta.env.MODE === "device";
|
||||||
export const isInCloud = !isOnDevice;
|
export const isInCloud = !isOnDevice;
|
||||||
|
|
||||||
export async function checkAuth() {
|
export async function checkAuth() {
|
||||||
const res = await fetch(`${import.meta.env.VITE_CLOUD_API}/me`, {
|
const res = await fetch(`${CLOUD_API}/me`, {
|
||||||
mode: "cors",
|
mode: "cors",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { LoaderFunctionArgs, redirect } from "react-router-dom";
|
import { LoaderFunctionArgs, redirect } from "react-router-dom";
|
||||||
import api from "../api";
|
import api from "../api";
|
||||||
|
import { CLOUD_API, CLOUD_APP, SIGNAL_API } from "@/ui.config";
|
||||||
|
|
||||||
const loader = async ({ request }: LoaderFunctionArgs) => {
|
const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
|
@ -11,17 +12,17 @@ const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||||
const clientId = searchParams.get("clientId");
|
const clientId = searchParams.get("clientId");
|
||||||
|
|
||||||
const res = await api.POST(
|
const res = await api.POST(
|
||||||
`${import.meta.env.VITE_SIGNAL_API}/cloud/register`,
|
`${SIGNAL_API}/cloud/register`,
|
||||||
{
|
{
|
||||||
token: tempToken,
|
token: tempToken,
|
||||||
cloudApi: import.meta.env.VITE_CLOUD_API,
|
cloudApi: CLOUD_API,
|
||||||
oidcGoogle,
|
oidcGoogle,
|
||||||
clientId,
|
clientId,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!res.ok) throw new Error("Failed to register device");
|
if (!res.ok) throw new Error("Failed to register device");
|
||||||
return redirect(import.meta.env.VITE_CLOUD_APP + `/devices/${deviceId}/setup`);
|
return redirect(CLOUD_APP + `/devices/${deviceId}/setup`);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function AdoptRoute() {
|
export default function AdoptRoute() {
|
||||||
|
|
|
@ -14,6 +14,7 @@ import { User } from "@/hooks/stores";
|
||||||
import { checkAuth } from "@/main";
|
import { checkAuth } from "@/main";
|
||||||
import Fieldset from "@components/Fieldset";
|
import Fieldset from "@components/Fieldset";
|
||||||
import { ChevronLeftIcon } from "@heroicons/react/16/solid";
|
import { ChevronLeftIcon } from "@heroicons/react/16/solid";
|
||||||
|
import { CLOUD_API } from "@/ui.config";
|
||||||
|
|
||||||
interface LoaderData {
|
interface LoaderData {
|
||||||
device: { id: string; name: string; user: { googleId: string } };
|
device: { id: string; name: string; user: { googleId: string } };
|
||||||
|
@ -24,7 +25,7 @@ const action = async ({ request }: ActionFunctionArgs) => {
|
||||||
const { deviceId } = Object.fromEntries(await request.formData());
|
const { deviceId } = Object.fromEntries(await request.formData());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${import.meta.env.VITE_CLOUD_API}/devices/${deviceId}`, {
|
const res = await fetch(`${CLOUD_API}/devices/${deviceId}`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
|
@ -46,7 +47,7 @@ const loader = async ({ params }: LoaderFunctionArgs) => {
|
||||||
const { id } = params;
|
const { id } = params;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${import.meta.env.VITE_CLOUD_API}/devices/${id}`, {
|
const res = await fetch(`${CLOUD_API}/devices/${id}`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
mode: "cors",
|
mode: "cors",
|
||||||
|
|
|
@ -16,6 +16,7 @@ import { User } from "@/hooks/stores";
|
||||||
import { checkAuth } from "@/main";
|
import { checkAuth } from "@/main";
|
||||||
import Fieldset from "@components/Fieldset";
|
import Fieldset from "@components/Fieldset";
|
||||||
import api from "../api";
|
import api from "../api";
|
||||||
|
import { CLOUD_API } from "@/ui.config";
|
||||||
|
|
||||||
interface LoaderData {
|
interface LoaderData {
|
||||||
device: { id: string; name: string; user: { googleId: string } };
|
device: { id: string; name: string; user: { googleId: string } };
|
||||||
|
@ -31,7 +32,7 @@ const action = async ({ params, request }: ActionFunctionArgs) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await api.PUT(`${import.meta.env.VITE_CLOUD_API}/devices/${id}`, {
|
const res = await api.PUT(`${CLOUD_API}/devices/${id}`, {
|
||||||
name,
|
name,
|
||||||
});
|
});
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
|
@ -49,7 +50,7 @@ const loader = async ({ params }: LoaderFunctionArgs) => {
|
||||||
const { id } = params;
|
const { id } = params;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${import.meta.env.VITE_CLOUD_API}/devices/${id}`, {
|
const res = await fetch(`${CLOUD_API}/devices/${id}`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
mode: "cors",
|
mode: "cors",
|
||||||
|
|
|
@ -16,10 +16,11 @@ import { InputFieldWithLabel } from "@components/InputField";
|
||||||
import { Button } from "@components/Button";
|
import { Button } from "@components/Button";
|
||||||
import { checkAuth } from "@/main";
|
import { checkAuth } from "@/main";
|
||||||
import api from "../api";
|
import api from "../api";
|
||||||
|
import { CLOUD_API } from "@/ui.config";
|
||||||
|
|
||||||
const loader = async ({ params }: LoaderFunctionArgs) => {
|
const loader = async ({ params }: LoaderFunctionArgs) => {
|
||||||
await checkAuth();
|
await checkAuth();
|
||||||
const res = await fetch(`${import.meta.env.VITE_CLOUD_API}/devices/${params.id}`, {
|
const res = await fetch(`${CLOUD_API}/devices/${params.id}`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
mode: "cors",
|
mode: "cors",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
|
@ -35,7 +36,7 @@ const loader = async ({ params }: LoaderFunctionArgs) => {
|
||||||
const action = async ({ request }: ActionFunctionArgs) => {
|
const action = async ({ request }: ActionFunctionArgs) => {
|
||||||
// Handle form submission
|
// Handle form submission
|
||||||
const { name, id, returnTo } = Object.fromEntries(await request.formData());
|
const { name, id, returnTo } = Object.fromEntries(await request.formData());
|
||||||
const res = await api.PUT(`${import.meta.env.VITE_CLOUD_API}/devices/${id}`, { name });
|
const res = await api.PUT(`${CLOUD_API}/devices/${id}`, { name });
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
return redirect(returnTo?.toString() ?? `/devices/${id}`);
|
return redirect(returnTo?.toString() ?? `/devices/${id}`);
|
||||||
|
|
|
@ -5,12 +5,12 @@ import {
|
||||||
HidState,
|
HidState,
|
||||||
UpdateState,
|
UpdateState,
|
||||||
useHidStore,
|
useHidStore,
|
||||||
|
useMountMediaStore,
|
||||||
User,
|
User,
|
||||||
useRTCStore,
|
useRTCStore,
|
||||||
useUiStore,
|
useUiStore,
|
||||||
useUpdateStore,
|
useUpdateStore,
|
||||||
useVideoStore,
|
useVideoStore,
|
||||||
useMountMediaStore,
|
|
||||||
VideoState,
|
VideoState,
|
||||||
} from "@/hooks/stores";
|
} from "@/hooks/stores";
|
||||||
import WebRTCVideo from "@components/WebRTCVideo";
|
import WebRTCVideo from "@components/WebRTCVideo";
|
||||||
|
@ -35,7 +35,8 @@ import api from "../api";
|
||||||
import { DeviceStatus } from "./welcome-local";
|
import { DeviceStatus } from "./welcome-local";
|
||||||
import FocusTrap from "focus-trap-react";
|
import FocusTrap from "focus-trap-react";
|
||||||
import OtherSessionConnectedModal from "@/components/OtherSessionConnectedModal";
|
import OtherSessionConnectedModal from "@/components/OtherSessionConnectedModal";
|
||||||
import TerminalWrapper from "../components/Terminal";
|
import Terminal from "@components/Terminal";
|
||||||
|
import { CLOUD_API, SIGNAL_API } from "@/ui.config";
|
||||||
|
|
||||||
interface LocalLoaderResp {
|
interface LocalLoaderResp {
|
||||||
authMode: "password" | "noPassword" | null;
|
authMode: "password" | "noPassword" | null;
|
||||||
|
@ -56,12 +57,12 @@ export interface LocalDevice {
|
||||||
|
|
||||||
const deviceLoader = async () => {
|
const deviceLoader = async () => {
|
||||||
const res = await api
|
const res = await api
|
||||||
.GET(`${import.meta.env.VITE_SIGNAL_API}/device/status`)
|
.GET(`${SIGNAL_API}/device/status`)
|
||||||
.then(res => res.json() as Promise<DeviceStatus>);
|
.then(res => res.json() as Promise<DeviceStatus>);
|
||||||
|
|
||||||
if (!res.isSetup) return redirect("/welcome");
|
if (!res.isSetup) return redirect("/welcome");
|
||||||
|
|
||||||
const deviceRes = await api.GET(`${import.meta.env.VITE_SIGNAL_API}/device`);
|
const deviceRes = await api.GET(`${SIGNAL_API}/device`);
|
||||||
if (deviceRes.status === 401) return redirect("/login-local");
|
if (deviceRes.status === 401) return redirect("/login-local");
|
||||||
if (deviceRes.ok) {
|
if (deviceRes.ok) {
|
||||||
const device = (await deviceRes.json()) as LocalDevice;
|
const device = (await deviceRes.json()) as LocalDevice;
|
||||||
|
@ -74,11 +75,11 @@ const deviceLoader = async () => {
|
||||||
const cloudLoader = async (params: Params<string>): Promise<CloudLoaderResp> => {
|
const cloudLoader = async (params: Params<string>): Promise<CloudLoaderResp> => {
|
||||||
const user = await checkAuth();
|
const user = await checkAuth();
|
||||||
|
|
||||||
const iceResp = await api.POST(`${import.meta.env.VITE_CLOUD_API}/webrtc/ice_config`);
|
const iceResp = await api.POST(`${CLOUD_API}/webrtc/ice_config`);
|
||||||
const iceConfig = await iceResp.json();
|
const iceConfig = await iceResp.json();
|
||||||
|
|
||||||
const deviceResp = await api.GET(
|
const deviceResp = await api.GET(
|
||||||
`${import.meta.env.VITE_CLOUD_API}/devices/${params.id}`,
|
`${CLOUD_API}/devices/${params.id}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!deviceResp.ok) {
|
if (!deviceResp.ok) {
|
||||||
|
@ -142,7 +143,7 @@ export default function KvmIdRoute() {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const sd = btoa(JSON.stringify(pc.localDescription));
|
const sd = btoa(JSON.stringify(pc.localDescription));
|
||||||
const res = await api.POST(`${import.meta.env.VITE_SIGNAL_API}/webrtc/session`, {
|
const res = await api.POST(`${SIGNAL_API}/webrtc/session`, {
|
||||||
sd,
|
sd,
|
||||||
// When on device, we don't need to specify the device id, as it's already known
|
// When on device, we don't need to specify the device id, as it's already known
|
||||||
...(isOnDevice ? {} : { id: params.id }),
|
...(isOnDevice ? {} : { id: params.id }),
|
||||||
|
@ -317,7 +318,7 @@ export default function KvmIdRoute() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fire and forget
|
// Fire and forget
|
||||||
api.POST(`${import.meta.env.VITE_CLOUD_API}/webrtc/turn_activity`, {
|
api.POST(`${CLOUD_API}/webrtc/turn_activity`, {
|
||||||
bytesReceived: bytesReceivedDelta,
|
bytesReceived: bytesReceivedDelta,
|
||||||
bytesSent: bytesSentDelta,
|
bytesSent: bytesSentDelta,
|
||||||
});
|
});
|
||||||
|
@ -327,6 +328,7 @@ export default function KvmIdRoute() {
|
||||||
const setHdmiState = useVideoStore(state => state.setHdmiState);
|
const setHdmiState = useVideoStore(state => state.setHdmiState);
|
||||||
|
|
||||||
const [hasUpdated, setHasUpdated] = useState(false);
|
const [hasUpdated, setHasUpdated] = useState(false);
|
||||||
|
|
||||||
function onJsonRpcRequest(resp: JsonRpcRequest) {
|
function onJsonRpcRequest(resp: JsonRpcRequest) {
|
||||||
if (resp.method === "otherSessionConnected") {
|
if (resp.method === "otherSessionConnected") {
|
||||||
console.log("otherSessionConnected", resp.params);
|
console.log("otherSessionConnected", resp.params);
|
||||||
|
@ -412,10 +414,39 @@ export default function KvmIdRoute() {
|
||||||
|
|
||||||
// System update
|
// System update
|
||||||
const disableKeyboardFocusTrap = useUiStore(state => state.disableVideoFocusTrap);
|
const disableKeyboardFocusTrap = useUiStore(state => state.disableVideoFocusTrap);
|
||||||
|
|
||||||
|
const [kvmTerminal, setKvmTerminal] = useState<RTCDataChannel | null>(null);
|
||||||
|
const [serialConsole, setSerialConsole] = useState<RTCDataChannel | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!peerConnection) return;
|
||||||
|
if (!kvmTerminal) {
|
||||||
|
console.log('Creating data channel "terminal"');
|
||||||
|
setKvmTerminal(peerConnection.createDataChannel("terminal"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!serialConsole) {
|
||||||
|
console.log('Creating data channel "serial"');
|
||||||
|
setSerialConsole(peerConnection.createDataChannel("serial"));
|
||||||
|
}
|
||||||
|
}, [kvmTerminal, peerConnection, serialConsole]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
kvmTerminal?.addEventListener("message", e => {
|
||||||
|
console.log(e.data);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
kvmTerminal?.removeEventListener("message", e => {
|
||||||
|
console.log(e.data);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}, [kvmTerminal]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Transition show={!isUpdateDialogOpen && otaState.updating}>
|
<Transition show={!isUpdateDialogOpen && otaState.updating}>
|
||||||
<div className="fixed inset-0 z-10 flex items-start justify-center w-full h-full max-w-xl mx-auto translate-y-8 pointer-events-none">
|
<div className="pointer-events-none fixed inset-0 z-10 mx-auto flex h-full w-full max-w-xl translate-y-8 items-start justify-center">
|
||||||
<div className="transition duration-1000 ease-in data-[closed]:opacity-0">
|
<div className="transition duration-1000 ease-in data-[closed]:opacity-0">
|
||||||
<UpdateInProgressStatusCard
|
<UpdateInProgressStatusCard
|
||||||
setIsUpdateDialogOpen={setIsUpdateDialogOpen}
|
setIsUpdateDialogOpen={setIsUpdateDialogOpen}
|
||||||
|
@ -424,7 +455,6 @@ export default function KvmIdRoute() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
|
|
||||||
<div className="relative h-full">
|
<div className="relative h-full">
|
||||||
<FocusTrap
|
<FocusTrap
|
||||||
paused={disableKeyboardFocusTrap}
|
paused={disableKeyboardFocusTrap}
|
||||||
|
@ -458,9 +488,7 @@ export default function KvmIdRoute() {
|
||||||
<OtherSessionConnectedModal
|
<OtherSessionConnectedModal
|
||||||
open={isOtherSessionConnectedModalOpen}
|
open={isOtherSessionConnectedModalOpen}
|
||||||
setOpen={state => {
|
setOpen={state => {
|
||||||
if (state === false) {
|
if (!state) connectWebRTC().then(r => r);
|
||||||
connectWebRTC();
|
|
||||||
}
|
|
||||||
|
|
||||||
// It takes some time for the WebRTC connection to be established, so we wait a bit before closing the modal
|
// It takes some time for the WebRTC connection to be established, so we wait a bit before closing the modal
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
@ -468,7 +496,12 @@ export default function KvmIdRoute() {
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<TerminalWrapper />
|
{kvmTerminal && (
|
||||||
|
<Terminal type="kvm" dataChannel={kvmTerminal} title="KVM Terminal" />
|
||||||
|
)}
|
||||||
|
{serialConsole && (
|
||||||
|
<Terminal type="serial" dataChannel={serialConsole} title="Serial Console" />
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { User } from "@/hooks/stores";
|
||||||
import EmptyCard from "@components/EmptyCard";
|
import EmptyCard from "@components/EmptyCard";
|
||||||
import { LuMonitorSmartphone } from "react-icons/lu";
|
import { LuMonitorSmartphone } from "react-icons/lu";
|
||||||
import { ArrowRightIcon } from "@heroicons/react/16/solid";
|
import { ArrowRightIcon } from "@heroicons/react/16/solid";
|
||||||
|
import { CLOUD_API } from "@/ui.config";
|
||||||
|
|
||||||
interface LoaderData {
|
interface LoaderData {
|
||||||
devices: { id: string; name: string; online: boolean; lastSeen: string }[];
|
devices: { id: string; name: string; online: boolean; lastSeen: string }[];
|
||||||
|
@ -19,7 +20,7 @@ export const loader = async () => {
|
||||||
const user = await checkAuth();
|
const user = await checkAuth();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${import.meta.env.VITE_CLOUD_API}/devices`, {
|
const res = await fetch(`${CLOUD_API}/devices`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
mode: "cors",
|
mode: "cors",
|
||||||
|
|
|
@ -12,15 +12,16 @@ import LogoWhiteIcon from "@/assets/logo-white.svg";
|
||||||
import api from "../api";
|
import api from "../api";
|
||||||
import { DeviceStatus } from "./welcome-local";
|
import { DeviceStatus } from "./welcome-local";
|
||||||
import ExtLink from "../components/ExtLink";
|
import ExtLink from "../components/ExtLink";
|
||||||
|
import { SIGNAL_API } from "@/ui.config";
|
||||||
|
|
||||||
const loader = async () => {
|
const loader = async () => {
|
||||||
const res = await api
|
const res = await api
|
||||||
.GET(`${import.meta.env.VITE_SIGNAL_API}/device/status`)
|
.GET(`${SIGNAL_API}/device/status`)
|
||||||
.then(res => res.json() as Promise<DeviceStatus>);
|
.then(res => res.json() as Promise<DeviceStatus>);
|
||||||
|
|
||||||
if (!res.isSetup) return redirect("/welcome");
|
if (!res.isSetup) return redirect("/welcome");
|
||||||
|
|
||||||
const deviceRes = await api.GET(`${import.meta.env.VITE_SIGNAL_API}/device`);
|
const deviceRes = await api.GET(`${SIGNAL_API}/device`);
|
||||||
if (deviceRes.ok) return redirect("/");
|
if (deviceRes.ok) return redirect("/");
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
@ -31,7 +32,7 @@ const action = async ({ request }: ActionFunctionArgs) => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await api.POST(
|
const response = await api.POST(
|
||||||
`${import.meta.env.VITE_SIGNAL_API}/auth/login-local`,
|
`${SIGNAL_API}/auth/login-local`,
|
||||||
{
|
{
|
||||||
password,
|
password,
|
||||||
},
|
},
|
||||||
|
|
|
@ -9,10 +9,11 @@ import LogoWhiteIcon from "@/assets/logo-white.svg";
|
||||||
import { cx } from "../cva.config";
|
import { cx } from "../cva.config";
|
||||||
import api from "../api";
|
import api from "../api";
|
||||||
import { DeviceStatus } from "./welcome-local";
|
import { DeviceStatus } from "./welcome-local";
|
||||||
|
import { SIGNAL_API } from "@/ui.config";
|
||||||
|
|
||||||
const loader = async () => {
|
const loader = async () => {
|
||||||
const res = await api
|
const res = await api
|
||||||
.GET(`${import.meta.env.VITE_SIGNAL_API}/device/status`)
|
.GET(`${SIGNAL_API}/device/status`)
|
||||||
.then(res => res.json() as Promise<DeviceStatus>);
|
.then(res => res.json() as Promise<DeviceStatus>);
|
||||||
|
|
||||||
if (res.isSetup) return redirect("/login-local");
|
if (res.isSetup) return redirect("/login-local");
|
||||||
|
@ -30,7 +31,7 @@ const action = async ({ request }: ActionFunctionArgs) => {
|
||||||
|
|
||||||
if (localAuthMode === "noPassword") {
|
if (localAuthMode === "noPassword") {
|
||||||
try {
|
try {
|
||||||
await api.POST(`${import.meta.env.VITE_SIGNAL_API}/device/setup`, {
|
await api.POST(`${SIGNAL_API}/device/setup`, {
|
||||||
localAuthMode,
|
localAuthMode,
|
||||||
});
|
});
|
||||||
return redirect("/");
|
return redirect("/");
|
||||||
|
|
|
@ -10,10 +10,11 @@ import LogoBlueIcon from "@/assets/logo-blue.png";
|
||||||
import LogoWhiteIcon from "@/assets/logo-white.svg";
|
import LogoWhiteIcon from "@/assets/logo-white.svg";
|
||||||
import api from "../api";
|
import api from "../api";
|
||||||
import { DeviceStatus } from "./welcome-local";
|
import { DeviceStatus } from "./welcome-local";
|
||||||
|
import { SIGNAL_API } from "@/ui.config";
|
||||||
|
|
||||||
const loader = async () => {
|
const loader = async () => {
|
||||||
const res = await api
|
const res = await api
|
||||||
.GET(`${import.meta.env.VITE_SIGNAL_API}/device/status`)
|
.GET(`${SIGNAL_API}/device/status`)
|
||||||
.then(res => res.json() as Promise<DeviceStatus>);
|
.then(res => res.json() as Promise<DeviceStatus>);
|
||||||
|
|
||||||
if (res.isSetup) return redirect("/login-local");
|
if (res.isSetup) return redirect("/login-local");
|
||||||
|
@ -30,7 +31,7 @@ const action = async ({ request }: ActionFunctionArgs) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await api.POST(`${import.meta.env.VITE_SIGNAL_API}/device/setup`, {
|
const response = await api.POST(`${SIGNAL_API}/device/setup`, {
|
||||||
localAuthMode: "password",
|
localAuthMode: "password",
|
||||||
password,
|
password,
|
||||||
});
|
});
|
||||||
|
|
|
@ -9,6 +9,7 @@ import LogoMark from "@/assets/logo-mark.png";
|
||||||
import { cx } from "cva";
|
import { cx } from "cva";
|
||||||
import api from "../api";
|
import api from "../api";
|
||||||
import { redirect } from "react-router-dom";
|
import { redirect } from "react-router-dom";
|
||||||
|
import { SIGNAL_API } from "@/ui.config";
|
||||||
|
|
||||||
export interface DeviceStatus {
|
export interface DeviceStatus {
|
||||||
isSetup: boolean;
|
isSetup: boolean;
|
||||||
|
@ -16,7 +17,7 @@ export interface DeviceStatus {
|
||||||
|
|
||||||
const loader = async () => {
|
const loader = async () => {
|
||||||
const res = await api
|
const res = await api
|
||||||
.GET(`${import.meta.env.VITE_SIGNAL_API}/device/status`)
|
.GET(`${SIGNAL_API}/device/status`)
|
||||||
.then(res => res.json() as Promise<DeviceStatus>);
|
.then(res => res.json() as Promise<DeviceStatus>);
|
||||||
|
|
||||||
if (res.isSetup) return redirect("/login-local");
|
if (res.isSetup) return redirect("/login-local");
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
interface JetKVMConfig {
|
||||||
|
CLOUD_API?: string;
|
||||||
|
CLOUD_APP?: string;
|
||||||
|
DEVICE_VERSION?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window { JETKVM_CONFIG?: JetKVMConfig; }
|
||||||
|
}
|
||||||
|
|
||||||
|
const getAppURL = (api_url?: string) => {
|
||||||
|
if (!api_url) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const url = new URL(api_url);
|
||||||
|
url.host = url.host.replace(/api\./, "app.");
|
||||||
|
// remove the ending slash
|
||||||
|
return url.toString().replace(/\/$/, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CLOUD_API = window.JETKVM_CONFIG?.CLOUD_API || import.meta.env.VITE_CLOUD_API;
|
||||||
|
export const CLOUD_APP = window.JETKVM_CONFIG?.CLOUD_APP || getAppURL(CLOUD_API) || import.meta.env.VITE_CLOUD_APP;
|
||||||
|
export const SIGNAL_API = import.meta.env.VITE_SIGNAL_API;
|
|
@ -2,13 +2,31 @@ import { defineConfig } from "vite";
|
||||||
import react from "@vitejs/plugin-react-swc";
|
import react from "@vitejs/plugin-react-swc";
|
||||||
import tsconfigPaths from "vite-tsconfig-paths";
|
import tsconfigPaths from "vite-tsconfig-paths";
|
||||||
|
|
||||||
export default defineConfig(({ mode }) => {
|
declare const process: {
|
||||||
|
env: {
|
||||||
|
JETKVM_PROXY_URL: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default defineConfig(({ mode, command }) => {
|
||||||
const isCloud = mode === "production";
|
const isCloud = mode === "production";
|
||||||
const onDevice = mode === "device";
|
const onDevice = mode === "device";
|
||||||
|
const { JETKVM_PROXY_URL } = process.env;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
plugins: [tsconfigPaths(), react()],
|
plugins: [tsconfigPaths(), react()],
|
||||||
build: { outDir: isCloud ? "dist" : "../static" },
|
build: { outDir: isCloud ? "dist" : "../static" },
|
||||||
server: { host: "0.0.0.0" },
|
server: {
|
||||||
base: onDevice ? "/static" : "/",
|
host: "0.0.0.0",
|
||||||
|
proxy: JETKVM_PROXY_URL ? {
|
||||||
|
'/me': JETKVM_PROXY_URL,
|
||||||
|
'/device': JETKVM_PROXY_URL,
|
||||||
|
'/webrtc': JETKVM_PROXY_URL,
|
||||||
|
'/auth': JETKVM_PROXY_URL,
|
||||||
|
'/storage': JETKVM_PROXY_URL,
|
||||||
|
'/cloud': JETKVM_PROXY_URL,
|
||||||
|
} : undefined
|
||||||
|
},
|
||||||
|
base: onDevice && command === 'build' ? "/static" : "/",
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
4
usb.go
4
usb.go
|
@ -132,7 +132,7 @@ func writeGadgetConfig() error {
|
||||||
}
|
}
|
||||||
err = writeGadgetAttrs(hid0Path, [][]string{
|
err = writeGadgetAttrs(hid0Path, [][]string{
|
||||||
{"protocol", "1"},
|
{"protocol", "1"},
|
||||||
{"subclass", "0"},
|
{"subclass", "1"},
|
||||||
{"report_length", "8"},
|
{"report_length", "8"},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -152,7 +152,7 @@ func writeGadgetConfig() error {
|
||||||
}
|
}
|
||||||
err = writeGadgetAttrs(hid1Path, [][]string{
|
err = writeGadgetAttrs(hid1Path, [][]string{
|
||||||
{"protocol", "2"},
|
{"protocol", "2"},
|
||||||
{"subclass", "0"},
|
{"subclass", "1"},
|
||||||
{"report_length", "6"},
|
{"report_length", "6"},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
38
web.go
38
web.go
|
@ -2,6 +2,8 @@ package kvm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"embed"
|
"embed"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
@ -77,6 +79,9 @@ func setupRouter() *gin.Engine {
|
||||||
// We use this to determine if the device is setup
|
// We use this to determine if the device is setup
|
||||||
r.GET("/device/status", handleDeviceStatus)
|
r.GET("/device/status", handleDeviceStatus)
|
||||||
|
|
||||||
|
// We use this to provide the UI with the device configuration
|
||||||
|
r.GET("/device/ui-config.js", handleDeviceUIConfig)
|
||||||
|
|
||||||
// We use this to setup the device in the welcome page
|
// We use this to setup the device in the welcome page
|
||||||
r.POST("/device/setup", handleSetup)
|
r.POST("/device/setup", handleSetup)
|
||||||
|
|
||||||
|
@ -142,8 +147,6 @@ func handleWebRTCSession(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleLogin(c *gin.Context) {
|
func handleLogin(c *gin.Context) {
|
||||||
LoadConfig()
|
|
||||||
|
|
||||||
if config.LocalAuthMode == "noPassword" {
|
if config.LocalAuthMode == "noPassword" {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Login is disabled in noPassword mode"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Login is disabled in noPassword mode"})
|
||||||
return
|
return
|
||||||
|
@ -156,7 +159,6 @@ func handleLogin(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
LoadConfig()
|
|
||||||
err := bcrypt.CompareHashAndPassword([]byte(config.HashedPassword), []byte(req.Password))
|
err := bcrypt.CompareHashAndPassword([]byte(config.HashedPassword), []byte(req.Password))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid password"})
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid password"})
|
||||||
|
@ -172,7 +174,6 @@ func handleLogin(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleLogout(c *gin.Context) {
|
func handleLogout(c *gin.Context) {
|
||||||
LoadConfig()
|
|
||||||
config.LocalAuthToken = ""
|
config.LocalAuthToken = ""
|
||||||
if err := SaveConfig(); err != nil {
|
if err := SaveConfig(); err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save configuration"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save configuration"})
|
||||||
|
@ -186,8 +187,6 @@ func handleLogout(c *gin.Context) {
|
||||||
|
|
||||||
func protectedMiddleware() gin.HandlerFunc {
|
func protectedMiddleware() gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
LoadConfig()
|
|
||||||
|
|
||||||
if config.LocalAuthMode == "noPassword" {
|
if config.LocalAuthMode == "noPassword" {
|
||||||
c.Next()
|
c.Next()
|
||||||
return
|
return
|
||||||
|
@ -216,8 +215,6 @@ func RunWebServer() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleDevice(c *gin.Context) {
|
func handleDevice(c *gin.Context) {
|
||||||
LoadConfig()
|
|
||||||
|
|
||||||
response := LocalDevice{
|
response := LocalDevice{
|
||||||
AuthMode: &config.LocalAuthMode,
|
AuthMode: &config.LocalAuthMode,
|
||||||
DeviceID: GetDeviceID(),
|
DeviceID: GetDeviceID(),
|
||||||
|
@ -227,8 +224,6 @@ func handleDevice(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleCreatePassword(c *gin.Context) {
|
func handleCreatePassword(c *gin.Context) {
|
||||||
LoadConfig()
|
|
||||||
|
|
||||||
if config.HashedPassword != "" {
|
if config.HashedPassword != "" {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Password already set"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Password already set"})
|
||||||
return
|
return
|
||||||
|
@ -269,8 +264,6 @@ func handleCreatePassword(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleUpdatePassword(c *gin.Context) {
|
func handleUpdatePassword(c *gin.Context) {
|
||||||
LoadConfig()
|
|
||||||
|
|
||||||
if config.HashedPassword == "" {
|
if config.HashedPassword == "" {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Password is not set"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Password is not set"})
|
||||||
return
|
return
|
||||||
|
@ -314,8 +307,6 @@ func handleUpdatePassword(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleDeletePassword(c *gin.Context) {
|
func handleDeletePassword(c *gin.Context) {
|
||||||
LoadConfig()
|
|
||||||
|
|
||||||
if config.HashedPassword == "" {
|
if config.HashedPassword == "" {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Password is not set"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Password is not set"})
|
||||||
return
|
return
|
||||||
|
@ -352,8 +343,6 @@ func handleDeletePassword(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleDeviceStatus(c *gin.Context) {
|
func handleDeviceStatus(c *gin.Context) {
|
||||||
LoadConfig()
|
|
||||||
|
|
||||||
response := DeviceStatus{
|
response := DeviceStatus{
|
||||||
IsSetup: config.LocalAuthMode != "",
|
IsSetup: config.LocalAuthMode != "",
|
||||||
}
|
}
|
||||||
|
@ -361,9 +350,22 @@ func handleDeviceStatus(c *gin.Context) {
|
||||||
c.JSON(http.StatusOK, response)
|
c.JSON(http.StatusOK, response)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleSetup(c *gin.Context) {
|
func handleDeviceUIConfig(c *gin.Context) {
|
||||||
LoadConfig()
|
config, _ := json.Marshal(gin.H{
|
||||||
|
"CLOUD_API": config.CloudURL,
|
||||||
|
"DEVICE_VERSION": builtAppVersion,
|
||||||
|
})
|
||||||
|
if config == nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to marshal config"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response := fmt.Sprintf("window.JETKVM_CONFIG = %s;", config)
|
||||||
|
|
||||||
|
c.Data(http.StatusOK, "text/javascript; charset=utf-8", []byte(response))
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleSetup(c *gin.Context) {
|
||||||
// Check if the device is already set up
|
// Check if the device is already set up
|
||||||
if config.LocalAuthMode != "" || config.HashedPassword != "" {
|
if config.LocalAuthMode != "" || config.HashedPassword != "" {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Device is already set up"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Device is already set up"})
|
||||||
|
|
|
@ -113,6 +113,8 @@ func newSession(config SessionConfig) (*Session, error) {
|
||||||
d.OnMessage(onDiskMessage)
|
d.OnMessage(onDiskMessage)
|
||||||
case "terminal":
|
case "terminal":
|
||||||
handleTerminalChannel(d)
|
handleTerminalChannel(d)
|
||||||
|
case "serial":
|
||||||
|
handleSerialChannel(d)
|
||||||
default:
|
default:
|
||||||
if strings.HasPrefix(d.Label(), uploadIdPrefix) {
|
if strings.HasPrefix(d.Label(), uploadIdPrefix) {
|
||||||
go handleUploadChannel(d)
|
go handleUploadChannel(d)
|
||||||
|
|
Loading…
Reference in New Issue