From cd333c4ebc748f157bc010dff13bf81ace20fd90 Mon Sep 17 00:00:00 2001 From: Aveline <352441+ym@users.noreply.github.com> Date: Mon, 17 Feb 2025 18:37:47 +0100 Subject: [PATCH] feat(extension): ATX/DC/Serial extension support --- config.go | 6 +- go.mod | 4 +- go.sum | 8 +- jsonrpc.go | 178 ++++++- main.go | 1 + serial.go | 269 +++++++++++ ui/.env.staging | 4 + ui/package-lock.json | 439 +++++++++--------- ui/package.json | 34 +- ui/src/components/ActionBar.tsx | 51 +- ui/src/components/Button.tsx | 14 +- ui/src/components/Terminal.tsx | 187 +++++++- ui/src/components/Xterm.tsx | 201 -------- .../components/extensions/ATXPowerControl.tsx | 171 +++++++ .../components/extensions/DCPowerControl.tsx | 114 +++++ .../components/extensions/SerialConsole.tsx | 130 ++++++ .../components/popovers/ExtensionPopover.tsx | 145 ++++++ ui/src/hooks/stores.ts | 9 +- ui/src/routes/devices.$id.tsx | 48 +- webrtc.go | 2 + 20 files changed, 1535 insertions(+), 480 deletions(-) create mode 100644 serial.go create mode 100644 ui/.env.staging delete mode 100644 ui/src/components/Xterm.tsx create mode 100644 ui/src/components/extensions/ATXPowerControl.tsx create mode 100644 ui/src/components/extensions/DCPowerControl.tsx create mode 100644 ui/src/components/extensions/SerialConsole.tsx create mode 100644 ui/src/components/popovers/ExtensionPopover.tsx diff --git a/config.go b/config.go index 435b87e..a09c426 100644 --- a/config.go +++ b/config.go @@ -22,8 +22,9 @@ type Config struct { LocalAuthToken string `json:"local_auth_token"` LocalAuthMode string `json:"localAuthMode"` //TODO: fix it with migration WakeOnLanDevices []WakeOnLanDevice `json:"wake_on_lan_devices"` - EdidString string `json:"hdmi_edid_string"` - DisplayMaxBrightness int `json:"display_max_brightness"` + 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"` } @@ -33,6 +34,7 @@ const configPath = "/userdata/kvm_config.json" var defaultConfig = &Config{ CloudURL: "https://api.jetkvm.com", AutoUpdateEnabled: true, // Set a default value + ActiveExtension: "", DisplayMaxBrightness: 64, DisplayDimAfterSec: 120, // 2 minutes DisplayOffAfterSec: 1800, // 30 minutes diff --git a/go.mod b/go.mod index a1c9f87..adc054a 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,7 @@ require ( github.com/pojntfx/go-nbd v0.3.2 github.com/psanford/httpreadat v0.1.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/net v0.30.0 ) @@ -33,6 +34,7 @@ require ( github.com/bytedance/sonic/loader v0.1.1 // indirect github.com/cloudwego/base64x v0.1.4 // 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/gin-contrib/sse v0.1.0 // indirect github.com/go-jose/go-jose/v4 v4.0.2 // indirect @@ -69,7 +71,7 @@ require ( github.com/wlynxg/anet v0.0.5 // indirect golang.org/x/arch v0.8.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 google.golang.org/protobuf v1.34.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect diff --git a/go.sum b/go.sum index 0b3c219..b7b8756 100644 --- a/go.sum +++ b/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/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/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.23 h1:4M6+isWdcStXEf15G/RbrMPOQj1dZ7HPZCGwE4kOeP0= github.com/creack/pty v1.1.23/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= @@ -146,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/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= +go.bug.st/serial v1.6.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.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= @@ -161,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.6.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.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +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/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= google.golang.org/protobuf v1.34.0 h1:Qo/qEd2RZPCf2nKuorzksSknv0d3ERwp1vFG38gSmH4= diff --git a/jsonrpc.go b/jsonrpc.go index 45ed56e..aa39d25 100644 --- a/jsonrpc.go +++ b/jsonrpc.go @@ -10,8 +10,11 @@ import ( "os/exec" "path/filepath" "reflect" + "strconv" + "time" "github.com/pion/webrtc/v4" + "go.bug.st/serial" ) type JSONRPCRequest struct { @@ -569,7 +572,172 @@ func rpcResetConfig() error { 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{ "ping": {Func: rpcPing}, "getDeviceID": {Func: rpcGetDeviceID}, @@ -618,4 +786,12 @@ var rpcHandlers = map[string]RPCHandler{ "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"}}, } diff --git a/main.go b/main.go index 7ff771f..e23e9c8 100644 --- a/main.go +++ b/main.go @@ -71,6 +71,7 @@ func Main() { if config.CloudToken != "" { go RunWebsocketClient() } + initSerialPort() sigs := make(chan os.Signal, 1) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) <-sigs diff --git a/serial.go b/serial.go new file mode 100644 index 0000000..3ad56d3 --- /dev/null +++ b/serial.go @@ -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() { + + }) +} diff --git a/ui/.env.staging b/ui/.env.staging new file mode 100644 index 0000000..651e5bc --- /dev/null +++ b/ui/.env.staging @@ -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 \ No newline at end of file diff --git a/ui/package-lock.json b/ui/package-lock.json index e60ce6f..0ba5323 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -8,17 +8,18 @@ "name": "kvm-ui", "version": "0.0.0", "dependencies": { - "@headlessui/react": "^2.1.10", - "@headlessui/tailwindcss": "^0.2.0", - "@heroicons/react": "^2.1.3", + "@headlessui/react": "^2.2.0", + "@headlessui/tailwindcss": "^0.2.1", + "@heroicons/react": "^2.2.0", "@xterm/addon-clipboard": "^0.1.0", "@xterm/addon-fit": "^0.10.0", "@xterm/addon-unicode11": "^0.8.0", "@xterm/addon-web-links": "^0.11.0", "@xterm/addon-webgl": "^0.18.0", + "@xterm/xterm": "^5.5.0", "cva": "^1.0.0-beta.1", "focus-trap-react": "^10.2.3", - "framer-motion": "^11.0.28", + "framer-motion": "^11.15.0", "lodash.throttle": "^4.1.1", "mini-svg-data-uri": "^1.4.4", "react": "^18.2.0", @@ -28,32 +29,33 @@ "react-icons": "^5.4.0", "react-router-dom": "^6.22.3", "react-simple-keyboard": "^3.7.112", - "recharts": "^2.12.6", - "tailwind-merge": "^2.2.2", + "react-xtermjs": "^1.0.9", + "recharts": "^2.15.0", + "tailwind-merge": "^2.5.5", "usehooks-ts": "^3.1.0", "validator": "^13.12.0", "xterm": "^5.3.0", "zustand": "^4.5.2" }, "devDependencies": { - "@tailwindcss/forms": "^0.5.7", - "@tailwindcss/typography": "^0.5.12", + "@tailwindcss/forms": "^0.5.9", + "@tailwindcss/typography": "^0.5.15", "@types/react": "^18.2.66", "@types/react-dom": "^18.3.0", "@types/validator": "^13.12.2", "@typescript-eslint/eslint-plugin": "^7.2.0", "@typescript-eslint/parser": "^7.2.0", - "@vitejs/plugin-react-swc": "^3.5.0", - "autoprefixer": "^10.4.19", + "@vitejs/plugin-react-swc": "^3.7.2", + "autoprefixer": "^10.4.20", "eslint": "^8.57.0", "eslint-plugin-react": "^7.34.1", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.6", - "postcss": "^8.4.38", - "prettier": "^3.2.5", + "postcss": "^8.4.49", + "prettier": "^3.4.2", "prettier-plugin-tailwindcss": "^0.5.13", - "tailwindcss": "^3.4.3", - "typescript": "^5.2.2", + "tailwindcss": "^3.4.17", + "typescript": "^5.7.2", "vite": "^5.2.0", "vite-tsconfig-paths": "^4.3.2" }, @@ -587,9 +589,9 @@ "integrity": "sha512-X8R8Oj771YRl/w+c1HqAC1szL8zWQRwFvgDwT129k9ACdBoud/+/rX9V0qiMl6LWUdP9voC2nDVZYPMQQsb6eA==" }, "node_modules/@headlessui/react": { - "version": "2.1.10", - "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.1.10.tgz", - "integrity": "sha512-6mLa2fjMDAFQi+/R10B+zU3edsUk/MDtENB2zHho0lqKU1uzhAfJLUduWds4nCo8wbl3vULtC5rJfZAQ1yqIng==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.0.tgz", + "integrity": "sha512-RzCEg+LXsuI7mHiSomsu/gBJSjpupm6A1qIZ5sWjd7JhARNlMiSA4kKfJpCKwU9tE+zMRterhhrP74PvfJrpXQ==", "dependencies": { "@floating-ui/react": "^0.26.16", "@react-aria/focus": "^3.17.1", @@ -600,14 +602,14 @@ "node": ">=10" }, "peerDependencies": { - "react": "^18", - "react-dom": "^18" + "react": "^18 || ^19 || ^19.0.0-rc", + "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "node_modules/@headlessui/tailwindcss": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@headlessui/tailwindcss/-/tailwindcss-0.2.0.tgz", - "integrity": "sha512-fpL830Fln1SykOCboExsWr3JIVeQKieLJ3XytLe/tt1A0XzqUthOftDmjcCYLW62w7mQI7wXcoPXr3tZ9QfGxw==", + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@headlessui/tailwindcss/-/tailwindcss-0.2.1.tgz", + "integrity": "sha512-2+5+NZ+RzMyrVeCZOxdbvkUSssSxGvcUxphkIfSVLpRiKsj+/63T2TOL9dBYMXVfj/CGr6hMxSRInzXv6YY7sA==", "engines": { "node": ">=10" }, @@ -616,11 +618,11 @@ } }, "node_modules/@heroicons/react": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.1.3.tgz", - "integrity": "sha512-fEcPfo4oN345SoqdlCDdSa4ivjaKbk0jTd+oubcgNxnNgAfzysfwWfQUr+51wigiWHQQRiZNd1Ao0M5Y3M2EGg==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz", + "integrity": "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==", "peerDependencies": { - "react": ">= 16" + "react": ">= 16 || ^19.0.0-rc" } }, "node_modules/@humanwhocodes/config-array": { @@ -1084,14 +1086,14 @@ ] }, "node_modules/@swc/core": { - "version": "1.4.12", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.4.12.tgz", - "integrity": "sha512-QljRxTaUajSLB9ui93cZ38/lmThwIw/BPxjn+TphrYN6LPU3vu9/ykjgHtlpmaXDDcngL4K5i396E7iwwEUxYg==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.10.1.tgz", + "integrity": "sha512-rQ4dS6GAdmtzKiCRt3LFVxl37FaY1cgL9kSUTnhQ2xc3fmHOd7jdJK/V4pSZMG1ruGTd0bsi34O2R0Olg9Zo/w==", "dev": true, "hasInstallScript": true, "dependencies": { - "@swc/counter": "^0.1.2", - "@swc/types": "^0.1.5" + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.17" }, "engines": { "node": ">=10" @@ -1101,19 +1103,19 @@ "url": "https://opencollective.com/swc" }, "optionalDependencies": { - "@swc/core-darwin-arm64": "1.4.12", - "@swc/core-darwin-x64": "1.4.12", - "@swc/core-linux-arm-gnueabihf": "1.4.12", - "@swc/core-linux-arm64-gnu": "1.4.12", - "@swc/core-linux-arm64-musl": "1.4.12", - "@swc/core-linux-x64-gnu": "1.4.12", - "@swc/core-linux-x64-musl": "1.4.12", - "@swc/core-win32-arm64-msvc": "1.4.12", - "@swc/core-win32-ia32-msvc": "1.4.12", - "@swc/core-win32-x64-msvc": "1.4.12" + "@swc/core-darwin-arm64": "1.10.1", + "@swc/core-darwin-x64": "1.10.1", + "@swc/core-linux-arm-gnueabihf": "1.10.1", + "@swc/core-linux-arm64-gnu": "1.10.1", + "@swc/core-linux-arm64-musl": "1.10.1", + "@swc/core-linux-x64-gnu": "1.10.1", + "@swc/core-linux-x64-musl": "1.10.1", + "@swc/core-win32-arm64-msvc": "1.10.1", + "@swc/core-win32-ia32-msvc": "1.10.1", + "@swc/core-win32-x64-msvc": "1.10.1" }, "peerDependencies": { - "@swc/helpers": "^0.5.0" + "@swc/helpers": "*" }, "peerDependenciesMeta": { "@swc/helpers": { @@ -1122,9 +1124,9 @@ } }, "node_modules/@swc/core-darwin-arm64": { - "version": "1.4.12", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.4.12.tgz", - "integrity": "sha512-BZUUq91LGJsLI2BQrhYL3yARkcdN4TS3YGNS6aRYUtyeWrGCTKHL90erF2BMU2rEwZLLkOC/U899R4o4oiSHfA==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.10.1.tgz", + "integrity": "sha512-NyELPp8EsVZtxH/mEqvzSyWpfPJ1lugpTQcSlMduZLj1EASLO4sC8wt8hmL1aizRlsbjCX+r0PyL+l0xQ64/6Q==", "cpu": [ "arm64" ], @@ -1138,9 +1140,9 @@ } }, "node_modules/@swc/core-darwin-x64": { - "version": "1.4.12", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.4.12.tgz", - "integrity": "sha512-Wkk8rq1RwCOgg5ybTlfVtOYXLZATZ+QjgiBNM7pIn03A5/zZicokNTYd8L26/mifly2e74Dz34tlIZBT4aTGDA==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.10.1.tgz", + "integrity": "sha512-L4BNt1fdQ5ZZhAk5qoDfUnXRabDOXKnXBxMDJ+PWLSxOGBbWE6aJTnu4zbGjJvtot0KM46m2LPAPY8ttknqaZA==", "cpu": [ "x64" ], @@ -1154,9 +1156,9 @@ } }, "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.4.12", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.4.12.tgz", - "integrity": "sha512-8jb/SN67oTQ5KSThWlKLchhU6xnlAlnmnLCCOKK1xGtFS6vD+By9uL+qeEY2krV98UCRTf68WSmC0SLZhVoz5A==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.10.1.tgz", + "integrity": "sha512-Y1u9OqCHgvVp2tYQAJ7hcU9qO5brDMIrA5R31rwWQIAKDkJKtv3IlTHF0hrbWk1wPR0ZdngkQSJZple7G+Grvw==", "cpu": [ "arm" ], @@ -1170,9 +1172,9 @@ } }, "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.4.12", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.4.12.tgz", - "integrity": "sha512-DhW47DQEZKCdSq92v5F03rqdpjRXdDMqxfu4uAlZ9Uo1wJEGvY23e1SNmhji2sVHsZbBjSvoXoBLk0v00nSG8w==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.10.1.tgz", + "integrity": "sha512-tNQHO/UKdtnqjc7o04iRXng1wTUXPgVd8Y6LI4qIbHVoVPwksZydISjMcilKNLKIwOoUQAkxyJ16SlOAeADzhQ==", "cpu": [ "arm64" ], @@ -1186,9 +1188,9 @@ } }, "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.4.12", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.4.12.tgz", - "integrity": "sha512-PR57pT3TssnCRvdsaKNsxZy9N8rFg9AKA1U7W+LxbZ/7Z7PHc5PjxF0GgZpE/aLmU6xOn5VyQTlzjoamVkt05g==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.10.1.tgz", + "integrity": "sha512-x0L2Pd9weQ6n8dI1z1Isq00VHFvpBClwQJvrt3NHzmR+1wCT/gcYl1tp9P5xHh3ldM8Cn4UjWCw+7PaUgg8FcQ==", "cpu": [ "arm64" ], @@ -1202,9 +1204,9 @@ } }, "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.4.12", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.4.12.tgz", - "integrity": "sha512-HLZIWNHWuFIlH+LEmXr1lBiwGQeCshKOGcqbJyz7xpqTh7m2IPAxPWEhr/qmMTMsjluGxeIsLrcsgreTyXtgNA==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.10.1.tgz", + "integrity": "sha512-yyYEwQcObV3AUsC79rSzN9z6kiWxKAVJ6Ntwq2N9YoZqSPYph+4/Am5fM1xEQYf/kb99csj0FgOelomJSobxQA==", "cpu": [ "x64" ], @@ -1218,9 +1220,9 @@ } }, "node_modules/@swc/core-linux-x64-musl": { - "version": "1.4.12", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.4.12.tgz", - "integrity": "sha512-M5fBAtoOcpz2YQAFtNemrPod5BqmzAJc8pYtT3dVTn1MJllhmLHlphU8BQytvoGr1PHgJL8ZJBlBGdt70LQ7Mw==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.10.1.tgz", + "integrity": "sha512-tcaS43Ydd7Fk7sW5ROpaf2Kq1zR+sI5K0RM+0qYLYYurvsJruj3GhBCaiN3gkzd8m/8wkqNqtVklWaQYSDsyqA==", "cpu": [ "x64" ], @@ -1234,9 +1236,9 @@ } }, "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.4.12", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.4.12.tgz", - "integrity": "sha512-K8LjjgZ7VQFtM+eXqjfAJ0z+TKVDng3r59QYn7CL6cyxZI2brLU3lNknZcUFSouZD+gsghZI/Zb8tQjVk7aKDQ==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.10.1.tgz", + "integrity": "sha512-D3Qo1voA7AkbOzQ2UGuKNHfYGKL6eejN8VWOoQYtGHHQi1p5KK/Q7V1ku55oxXBsj79Ny5FRMqiRJpVGad7bjQ==", "cpu": [ "arm64" ], @@ -1250,9 +1252,9 @@ } }, "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.4.12", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.4.12.tgz", - "integrity": "sha512-hflO5LCxozngoOmiQbDPyvt6ODc5Cu9AwTJP9uH/BSMPdEQ6PCnefuUOJLAKew2q9o+NmDORuJk+vgqQz9Uzpg==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.10.1.tgz", + "integrity": "sha512-WalYdFoU3454Og+sDKHM1MrjvxUGwA2oralknXkXL8S0I/8RkWZOB++p3pLaGbTvOO++T+6znFbQdR8KRaa7DA==", "cpu": [ "ia32" ], @@ -1266,9 +1268,9 @@ } }, "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.4.12", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.4.12.tgz", - "integrity": "sha512-3A4qMtddBDbtprV5edTB/SgJn9L+X5TL7RGgS3eWtEgn/NG8gA80X/scjf1v2MMeOsrcxiYhnemI2gXCKuQN2g==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.10.1.tgz", + "integrity": "sha512-JWobfQDbTnoqaIwPKQ3DVSywihVXlQMbDuwik/dDWlj33A8oEHcjPOGs4OqcA3RHv24i+lfCQpM3Mn4FAMfacA==", "cpu": [ "x64" ], @@ -1296,30 +1298,30 @@ } }, "node_modules/@swc/types": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.6.tgz", - "integrity": "sha512-/JLo/l2JsT/LRd80C3HfbmVpxOAJ11FO2RCEslFrgzLltoP9j8XIbsyDcfCt2WWyX+CM96rBoNM+IToAkFOugg==", + "version": "0.1.17", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.17.tgz", + "integrity": "sha512-V5gRru+aD8YVyCOMAjMpWR1Ui577DD5KSJsHP8RAxopAH22jFz6GZd/qxqjO6MJHQhcsjvjOFXyDhyLQUnMveQ==", "dev": true, "dependencies": { "@swc/counter": "^0.1.3" } }, "node_modules/@tailwindcss/forms": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.7.tgz", - "integrity": "sha512-QE7X69iQI+ZXwldE+rzasvbJiyV/ju1FGHH0Qn2W3FKbuYtqp8LKcy6iSw79fVUT5/Vvf+0XgLCeYVG+UV6hOw==", + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.9.tgz", + "integrity": "sha512-tM4XVr2+UVTxXJzey9Twx48c1gcxFStqn1pQz0tRsX8o3DvxhN5oY5pvyAbUx7VTaZxpej4Zzvc6h+1RJBzpIg==", "dev": true, "dependencies": { "mini-svg-data-uri": "^1.2.3" }, "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": { - "version": "0.5.12", - "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.12.tgz", - "integrity": "sha512-CNwpBpconcP7ppxmuq3qvaCxiRWnbhANpY/ruH4L5qs2GCiVDJXde/pjj2HWPV1+Q4G9+V/etrwUYopdcjAlyg==", + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.15.tgz", + "integrity": "sha512-AqhlCXl+8grUz8uqExv5OTtgpjuVIwFTSXTrh8y9/pw6q2ek7fJ+Y8ZEVw7EB2DCcuCOtEjf9w3+J3rzts01uA==", "dev": true, "dependencies": { "lodash.castarray": "^4.4.0", @@ -1328,7 +1330,7 @@ "postcss-selector-parser": "6.0.10" }, "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": { @@ -1669,15 +1671,15 @@ "dev": true }, "node_modules/@vitejs/plugin-react-swc": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.6.0.tgz", - "integrity": "sha512-XFRbsGgpGxGzEV5i5+vRiro1bwcIaZDIdBRP16qwm+jP68ue/S8FJTBEgOeojtVDYrbSua3XFp71kC8VJE6v+g==", + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.7.2.tgz", + "integrity": "sha512-y0byko2b2tSVVf5Gpng1eEhX1OvPC7x8yns1Fx8jDzlJp4LS6CMkCPfLw47cjyoMrshQDoQw4qcgjsU9VvlCew==", "dev": true, "dependencies": { - "@swc/core": "^1.3.107" + "@swc/core": "^1.7.26" }, "peerDependencies": { - "vite": "^4 || ^5" + "vite": "^4 || ^5 || ^6" } }, "node_modules/@xterm/addon-clipboard": { @@ -1726,8 +1728,7 @@ "node_modules/@xterm/xterm": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", - "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", - "peer": true + "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==" }, "node_modules/acorn": { "version": "8.11.3", @@ -1965,9 +1966,9 @@ } }, "node_modules/autoprefixer": { - "version": "10.4.19", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz", - "integrity": "sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==", + "version": "10.4.20", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", + "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", "dev": true, "funding": [ { @@ -1984,11 +1985,11 @@ } ], "dependencies": { - "browserslist": "^4.23.0", - "caniuse-lite": "^1.0.30001599", + "browserslist": "^4.23.3", + "caniuse-lite": "^1.0.30001646", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", - "picocolors": "^1.0.0", + "picocolors": "^1.0.1", "postcss-value-parser": "^4.2.0" }, "bin": { @@ -2041,20 +2042,20 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" } }, "node_modules/browserslist": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", - "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.3.tgz", + "integrity": "sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA==", "dev": true, "funding": [ { @@ -2071,10 +2072,10 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001587", - "electron-to-chromium": "^1.4.668", - "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.13" + "caniuse-lite": "^1.0.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.1" }, "bin": { "browserslist": "cli.js" @@ -2120,9 +2121,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001666", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001666.tgz", - "integrity": "sha512-gD14ICmoV5ZZM1OdzPWmpx+q4GyefaK06zi8hmfHV5xe4/2nOQX3+Dw5o+fSqOws2xVwL9j+anOPFwHzdEdV4g==", + "version": "1.0.30001690", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz", + "integrity": "sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w==", "dev": true, "funding": [ { @@ -2547,9 +2548,9 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" }, "node_modules/electron-to-chromium": { - "version": "1.4.729", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.729.tgz", - "integrity": "sha512-bx7+5Saea/qu14kmPTDHQxkp2UnziG3iajUQu3BxFvCOnpAJdDbMV4rSl+EqFDkkpNNVUFlR1kDfpL59xfy1HA==", + "version": "1.5.75", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.75.tgz", + "integrity": "sha512-Lf3++DumRE/QmweGjU+ZcKqQ+3bKkU/qjaKYhIJKEOhgIO9Xs6IiAQFkfFoj+RhgDk4LUeNsLo6plExHqSyu6Q==", "dev": true }, "node_modules/emoji-regex": { @@ -2754,9 +2755,9 @@ } }, "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, "engines": { "node": ">=6" @@ -3112,9 +3113,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -3218,16 +3219,18 @@ } }, "node_modules/framer-motion": { - "version": "11.0.28", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.0.28.tgz", - "integrity": "sha512-j/vNYTCH5MX5sY/3dwMs00z1+qAqKX3iIHF762bwqlU814ooD5dDbuj3pA0LmIT5YqyryCkXEb/q+zRblin0lw==", + "version": "11.15.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.15.0.tgz", + "integrity": "sha512-MLk8IvZntxOMg7lDBLw2qgTHHv664bYoYmnFTmE0Gm/FW67aOJk0WM3ctMcG+Xhcv+vh5uyyXwxvxhSeJzSe+w==", "dependencies": { + "motion-dom": "^11.14.3", + "motion-utils": "^11.14.3", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", - "react": "^18.0.0", - "react-dom": "^18.0.0" + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "@emotion/is-prop-valid": { @@ -4021,9 +4024,9 @@ } }, "node_modules/jiti": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", - "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==", + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "bin": { "jiti": "bin/jiti.js" } @@ -4106,11 +4109,14 @@ } }, "node_modules/lilconfig": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", - "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", "engines": { - "node": ">=10" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" } }, "node_modules/lines-and-columns": { @@ -4198,11 +4204,11 @@ } }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { @@ -4239,6 +4245,16 @@ "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": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -4279,9 +4295,9 @@ "dev": true }, "node_modules/node-releases": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", "dev": true }, "node_modules/normalize-path": { @@ -4551,9 +4567,9 @@ } }, "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" }, "node_modules/picomatch": { "version": "2.3.1", @@ -4592,9 +4608,9 @@ } }, "node_modules/postcss": { - "version": "8.4.38", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", - "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "version": "8.4.49", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", + "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", "funding": [ { "type": "opencollective", @@ -4611,8 +4627,8 @@ ], "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.0.0", - "source-map-js": "^1.2.0" + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" }, "engines": { "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": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", - "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "dependencies": { - "postcss-selector-parser": "^6.0.11" + "postcss-selector-parser": "^6.1.1" }, "engines": { "node": ">=12.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, "peerDependencies": { "postcss": "^8.2.14" } }, "node_modules/postcss-selector-parser": { - "version": "6.0.16", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz", - "integrity": "sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==", + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -4758,9 +4769,9 @@ } }, "node_modules/prettier": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", - "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz", + "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==", "dev": true, "bin": { "prettier": "bin/prettier.cjs" @@ -5015,6 +5026,14 @@ "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": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -5035,14 +5054,14 @@ } }, "node_modules/recharts": { - "version": "2.12.6", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.12.6.tgz", - "integrity": "sha512-D+7j9WI+D0NHauah3fKHuNNcRK8bOypPW7os1DERinogGBGaHI7i6tQKJ0aUF3JXyBZ63dyfKIW2WTOPJDxJ8w==", + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.0.tgz", + "integrity": "sha512-cIvMxDfpAmqAmVgc4yb7pgm/O1tmmkl/CjrvXuW+62/+7jj/iF9Ykm+hb/UJt42TREHMyd3gb+pkgoa2MxgDIw==", "dependencies": { "clsx": "^2.0.0", "eventemitter3": "^4.0.1", "lodash": "^4.17.21", - "react-is": "^16.10.2", + "react-is": "^18.3.1", "react-smooth": "^4.0.0", "recharts-scale": "^0.4.4", "tiny-invariant": "^1.3.1", @@ -5052,8 +5071,8 @@ "node": ">=14" }, "peerDependencies": { - "react": "^16.0.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^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 || ^19.0.0" } }, "node_modules/recharts-scale": { @@ -5064,6 +5083,11 @@ "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": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz", @@ -5362,9 +5386,9 @@ } }, "node_modules/source-map-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", - "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "engines": { "node": ">=0.10.0" } @@ -5610,44 +5634,41 @@ "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==" }, "node_modules/tailwind-merge": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.2.2.tgz", - "integrity": "sha512-tWANXsnmJzgw6mQ07nE3aCDkCK4QdT3ThPMCzawoYA2Pws7vSTCvz3Vrjg61jVUGfFZPJzxEP+NimbcW+EdaDw==", - "dependencies": { - "@babel/runtime": "^7.24.0" - }, + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.5.tgz", + "integrity": "sha512-0LXunzzAZzo0tEPxV3I297ffKZPlKDrjj7NXphC8V5ak9yHC5zRmxnOe2m/Rd/7ivsOMJe3JZ2JVocoDdQTRBA==", "funding": { "type": "github", "url": "https://github.com/sponsors/dcastil" } }, "node_modules/tailwindcss": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.3.tgz", - "integrity": "sha512-U7sxQk/n397Bmx4JHbJx/iSOOv5G+II3f1kpLpY2QeUv5DcPdcTsYLlusZfq1NthHS1c1cZoyFmmkex1rzke0A==", + "version": "3.4.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", + "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", - "chokidar": "^3.5.3", + "chokidar": "^3.6.0", "didyoumean": "^1.2.2", "dlv": "^1.1.3", - "fast-glob": "^3.3.0", + "fast-glob": "^3.3.2", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", - "jiti": "^1.21.0", - "lilconfig": "^2.1.0", - "micromatch": "^4.0.5", + "jiti": "^1.21.6", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", - "picocolors": "^1.0.0", - "postcss": "^8.4.23", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.1", - "postcss-nested": "^6.0.1", - "postcss-selector-parser": "^6.0.11", - "resolve": "^1.22.2", - "sucrase": "^3.32.0" + "postcss-load-config": "^4.0.2", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" }, "bin": { "tailwind": "lib/cli.js", @@ -5854,9 +5875,9 @@ } }, "node_modules/typescript": { - "version": "5.4.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.4.tgz", - "integrity": "sha512-dGE2Vv8cpVvw28v8HCPqyb08EzbBURxDpuhJvTrusShUfGnhHBafDsLdS1EhhxyL6BJQE+2cT3dDPAv+MQ6oLw==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", "devOptional": true, "bin": { "tsc": "bin/tsc", @@ -5882,9 +5903,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", - "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", + "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", "dev": true, "funding": [ { @@ -5901,8 +5922,8 @@ } ], "dependencies": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" + "escalade": "^3.2.0", + "picocolors": "^1.1.0" }, "bin": { "update-browserslist-db": "cli.js" diff --git a/ui/package.json b/ui/package.json index 7d569e3..fcf1c55 100644 --- a/ui/package.json +++ b/ui/package.json @@ -11,21 +11,24 @@ "dev:cloud": "vite dev --mode=development", "build": "npm run build:prod", "build:device": "tsc && vite build --mode=device --emptyOutDir", + "build:staging": "tsc && vite build --mode=staging", "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": { - "@headlessui/react": "^2.1.10", - "@headlessui/tailwindcss": "^0.2.0", - "@heroicons/react": "^2.1.3", + "@headlessui/react": "^2.2.0", + "@headlessui/tailwindcss": "^0.2.1", + "@heroicons/react": "^2.2.0", "@xterm/addon-clipboard": "^0.1.0", "@xterm/addon-fit": "^0.10.0", "@xterm/addon-unicode11": "^0.8.0", "@xterm/addon-web-links": "^0.11.0", "@xterm/addon-webgl": "^0.18.0", + "@xterm/xterm": "^5.5.0", "cva": "^1.0.0-beta.1", "focus-trap-react": "^10.2.3", - "framer-motion": "^11.0.28", + "framer-motion": "^11.15.0", "lodash.throttle": "^4.1.1", "mini-svg-data-uri": "^1.4.4", "react": "^18.2.0", @@ -35,32 +38,33 @@ "react-icons": "^5.4.0", "react-router-dom": "^6.22.3", "react-simple-keyboard": "^3.7.112", - "recharts": "^2.12.6", - "tailwind-merge": "^2.2.2", + "react-xtermjs": "^1.0.9", + "recharts": "^2.15.0", + "tailwind-merge": "^2.5.5", "usehooks-ts": "^3.1.0", "validator": "^13.12.0", "xterm": "^5.3.0", "zustand": "^4.5.2" }, "devDependencies": { - "@tailwindcss/forms": "^0.5.7", - "@tailwindcss/typography": "^0.5.12", + "@tailwindcss/forms": "^0.5.9", + "@tailwindcss/typography": "^0.5.15", "@types/react": "^18.2.66", "@types/react-dom": "^18.3.0", "@types/validator": "^13.12.2", "@typescript-eslint/eslint-plugin": "^7.2.0", "@typescript-eslint/parser": "^7.2.0", - "@vitejs/plugin-react-swc": "^3.5.0", - "autoprefixer": "^10.4.19", + "@vitejs/plugin-react-swc": "^3.7.2", + "autoprefixer": "^10.4.20", "eslint": "^8.57.0", "eslint-plugin-react": "^7.34.1", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.6", - "postcss": "^8.4.38", - "prettier": "^3.2.5", + "postcss": "^8.4.49", + "prettier": "^3.4.2", "prettier-plugin-tailwindcss": "^0.5.13", - "tailwindcss": "^3.4.3", - "typescript": "^5.2.2", + "tailwindcss": "^3.4.17", + "typescript": "^5.7.2", "vite": "^5.2.0", "vite-tsconfig-paths": "^4.3.2" } diff --git a/ui/src/components/ActionBar.tsx b/ui/src/components/ActionBar.tsx index 13ab896..6558a55 100644 --- a/ui/src/components/ActionBar.tsx +++ b/ui/src/components/ActionBar.tsx @@ -2,13 +2,12 @@ import { Button } from "@components/Button"; import { useHidStore, useMountMediaStore, - useUiStore, useSettingsStore, - useVideoStore, + useUiStore, } from "@/hooks/stores"; import { MdOutlineContentPasteGo } from "react-icons/md"; 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 PasteModal from "@/components/popovers/PasteModal"; import { FaKeyboard } from "react-icons/fa6"; @@ -17,6 +16,7 @@ import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react"; import MountPopopover from "./popovers/MountPopover"; import { Fragment, useCallback, useRef } from "react"; import { CommandLineIcon } from "@heroicons/react/20/solid"; +import ExtensionPopover from "./popovers/ExtensionPopover"; export default function Actionbar({ requestFullscreen, @@ -28,13 +28,12 @@ export default function Actionbar({ const setVirtualKeyboard = useHidStore(state => state.setVirtualKeyboardEnabled); const toggleSidebarView = useUiStore(state => state.toggleSidebarView); const setDisableFocusTrap = useUiStore(state => state.setDisableVideoFocusTrap); - const enableTerminal = useUiStore(state => state.enableTerminal); - const setEnableTerminal = useUiStore(state => state.setEnableTerminal); + const terminalType = useUiStore(state => state.terminalType); + const setTerminalType = useUiStore(state => state.setTerminalType); const remoteVirtualMediaState = useMountMediaStore( state => state.remoteVirtualMediaState, ); const developerMode = useSettingsStore(state => state.developerMode); - const hdmiState = useVideoStore(state => state.hdmiState); // This is the only way to get a reliable state change for the popover // at time of writing this there is no mount, or unmount event for the popover @@ -55,7 +54,7 @@ export default function Actionbar({ ); return ( - +
e.stopPropagation()} onKeyDown={e => e.stopPropagation()} @@ -68,7 +67,7 @@ export default function Actionbar({ theme="light" text="Web Terminal" LeadingIcon={({ className }) => } - onClick={() => setEnableTerminal(!enableTerminal)} + onClick={() => setTerminalType(terminalType === "kvm" ? "none" : "kvm")} /> )} @@ -94,7 +93,7 @@ export default function Actionbar({ {({ open }) => { checkIfStateChanged(open); return ( -
+
); @@ -136,7 +135,7 @@ export default function Actionbar({ {({ open }) => { checkIfStateChanged(open); return ( -
+
); @@ -188,7 +187,7 @@ export default function Actionbar({ {({ open }) => { checkIfStateChanged(open); return ( -
+
); @@ -208,6 +207,33 @@ export default function Actionbar({
+ + +
-
+
+
- +
- +
); } -export default TerminalWrapper; +export default Terminal; diff --git a/ui/src/components/Xterm.tsx b/ui/src/components/Xterm.tsx deleted file mode 100644 index 1a0a008..0000000 --- a/ui/src/components/Xterm.tsx +++ /dev/null @@ -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(null); - const containerRef = useRef(null); - const terminalElmRef = useRef(null); - const fitAddonRef = useRef(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 ( -
-
-
- ); -} diff --git a/ui/src/components/extensions/ATXPowerControl.tsx b/ui/src/components/extensions/ATXPowerControl.tsx new file mode 100644 index 0000000..2d1323f --- /dev/null +++ b/ui/src/components/extensions/ATXPowerControl.tsx @@ -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 | null>(null); + const [atxState, setAtxState] = useState(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 ( +
+ + + {atxState === null ? ( + + + + ) : ( + +
+ {/* Control Buttons */} +
+
+ +
+ {/* Status Indicators */} +
+
+ + + Power LED + +
+
+ + + HDD LED + +
+
+
+
+ )} +
+ ); +} diff --git a/ui/src/components/extensions/DCPowerControl.tsx b/ui/src/components/extensions/DCPowerControl.tsx new file mode 100644 index 0000000..e4ba29d --- /dev/null +++ b/ui/src/components/extensions/DCPowerControl.tsx @@ -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(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 ( +
+ + + {powerState === null ? ( + + + + ) : ( + +
+ {/* Power Controls */} +
+
+
+ + {/* Status Display */} +
+
+ +

+ {powerState.voltage.toFixed(1)}V +

+
+
+ +

+ {powerState.current.toFixed(1)}A +

+
+
+ +

+ {powerState.power.toFixed(1)}W +

+
+
+
+
+ )} +
+ ); +} diff --git a/ui/src/components/extensions/SerialConsole.tsx b/ui/src/components/extensions/SerialConsole.tsx new file mode 100644 index 0000000..c57d364 --- /dev/null +++ b/ui/src/components/extensions/SerialConsole.tsx @@ -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({ + 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 ( +
+ + + +
+ {/* Open Console Button */} +
+
+
+ {/* Settings */} +
+ handleSettingChange("baudRate", e.target.value)} + /> + + handleSettingChange("dataBits", e.target.value)} + /> + + handleSettingChange("stopBits", e.target.value)} + /> + + handleSettingChange("parity", e.target.value)} + /> +
+
+
+
+ ); +} diff --git a/ui/src/components/popovers/ExtensionPopover.tsx b/ui/src/components/popovers/ExtensionPopover.tsx new file mode 100644 index 0000000..9438bdb --- /dev/null +++ b/ui/src/components/popovers/ExtensionPopover.tsx @@ -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(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 ; + case "dc-power": + return ; + case "serial-console": + return ; + default: + return null; + } + }; + + return ( + +
+
+
+ {activeExtension ? ( + // Extension Control View +
+ {renderActiveExtension()} + +
+
+
+ ) : ( + // Extensions List View +
+ + +
+ {AVAILABLE_EXTENSIONS.map(extension => ( +
+
+

+ {extension.name} +

+

+ {extension.description} +

+
+
+ ))} +
+
+
+ )} +
+
+
+
+ ); +} diff --git a/ui/src/hooks/stores.ts b/ui/src/hooks/stores.ts index 7a0268b..5b1366c 100644 --- a/ui/src/hooks/stores.ts +++ b/ui/src/hooks/stores.ts @@ -22,6 +22,7 @@ const appendStatToMap = ( // Constants and types export type AvailableSidebarViews = "system" | "connection-stats"; export type AvailableModalViews = "connection-stats" | "settings"; +export type AvailableTerminalTypes = "kvm" | "serial" | "none"; export interface User { sub: string; @@ -52,13 +53,13 @@ interface UIState { isAttachedVirtualKeyboardVisible: boolean; setAttachedVirtualKeyboardVisibility: (enabled: boolean) => void; - enableTerminal: boolean; - setEnableTerminal: (enabled: UIState["enableTerminal"]) => void; + terminalType: AvailableTerminalTypes; + setTerminalType: (enabled: UIState["terminalType"]) => void; } export const useUiStore = create(set => ({ - enableTerminal: false, - setEnableTerminal: enabled => set({ enableTerminal: enabled }), + terminalType: "none", + setTerminalType: type => set({ terminalType: type }), sidebarView: null, setSidebarView: view => set({ sidebarView: view }), diff --git a/ui/src/routes/devices.$id.tsx b/ui/src/routes/devices.$id.tsx index bd950b2..10ae0bc 100644 --- a/ui/src/routes/devices.$id.tsx +++ b/ui/src/routes/devices.$id.tsx @@ -5,12 +5,12 @@ import { HidState, UpdateState, useHidStore, + useMountMediaStore, User, useRTCStore, useUiStore, useUpdateStore, useVideoStore, - useMountMediaStore, VideoState, } from "@/hooks/stores"; import WebRTCVideo from "@components/WebRTCVideo"; @@ -35,7 +35,7 @@ import api from "../api"; import { DeviceStatus } from "./welcome-local"; import FocusTrap from "focus-trap-react"; 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 { @@ -328,6 +328,7 @@ export default function KvmIdRoute() { const setHdmiState = useVideoStore(state => state.setHdmiState); const [hasUpdated, setHasUpdated] = useState(false); + function onJsonRpcRequest(resp: JsonRpcRequest) { if (resp.method === "otherSessionConnected") { console.log("otherSessionConnected", resp.params); @@ -413,10 +414,39 @@ export default function KvmIdRoute() { // System update const disableKeyboardFocusTrap = useUiStore(state => state.disableVideoFocusTrap); + + const [kvmTerminal, setKvmTerminal] = useState(null); + const [serialConsole, setSerialConsole] = useState(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 ( <> -
+
-
{ - if (state === false) { - connectWebRTC(); - } + if (!state) connectWebRTC().then(r => r); // It takes some time for the WebRTC connection to be established, so we wait a bit before closing the modal setTimeout(() => { @@ -469,7 +496,12 @@ export default function KvmIdRoute() { }, 1000); }} /> - + {kvmTerminal && ( + + )} + {serialConsole && ( + + )} ); } diff --git a/webrtc.go b/webrtc.go index 27084fc..5e9ce3d 100644 --- a/webrtc.go +++ b/webrtc.go @@ -113,6 +113,8 @@ func newSession(config SessionConfig) (*Session, error) { d.OnMessage(onDiskMessage) case "terminal": handleTerminalChannel(d) + case "serial": + handleSerialChannel(d) default: if strings.HasPrefix(d.Label(), uploadIdPrefix) { go handleUploadChannel(d)